국비교육(22-23)

75일차(3)/Android App(32) : Shared Preferences(2) / menu

서리/Seori 2023. 1. 26. 00:38

75일차(3)/Android App(32) : Shared Preferences(2)

 

 

- 이전 예제와 같은 모듈에 새 액티비티 생성 (설정 액티비티)

 

- 이번에는 java로 생성!

 

 

SettingsActivity

package com.example.step13sharedpref;

import android.content.SharedPreferences;
import android.os.Bundle;
import android.widget.Toast;

import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;

public class SettingsActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.settings_activity);
        if (savedInstanceState == null) {
            //settings 라는 아이디를 가지고있는 레이아웃에 SettingsFragment 띄우기
            getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.settings, new SettingsFragment())
                    .commit();
        }
        //up button 이 액션바에 보이도록 설정
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
        }
        //preference 리스너 등록
        SharedPreferences pref= PreferenceManager.getDefaultSharedPreferences(this);
        pref.registerOnSharedPreferenceChangeListener(this);
    }

    //설정을 변경하면 호출되는 메소드
    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        //메소드에 전달되는 키값을 활용해서 변화된 값을 읽어온다.
        if(key.equals("signature")){
            String value = sharedPreferences.getString(key, "");
            Toast.makeText(this, value, Toast.LENGTH_SHORT).show();
        }else if(key.equals("reply")) {
            String value = sharedPreferences.getString(key, "");
            Toast.makeText(this, value, Toast.LENGTH_SHORT).show();
        } else if (key.equals("sync")) {
            Boolean value = sharedPreferences.getBoolean(key, false);
            Toast.makeText(this, "동기화 여부:"+value, Toast.LENGTH_SHORT).show();
        }
    }

    //내부 클래스 SettingsFragment (SharedPreferences 를 자동으로 사용하는 기능을 가진 fragment)
    public static class SettingsFragment extends PreferenceFragmentCompat {
        @Override
        public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
            //res/xml/root_preferences.xml 문서를 전개해서 fragment 설정 메뉴 구성하기
            setPreferencesFromResource(R.xml.root_preferences, rootKey);
        }
    }
}

 


 

AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyAndroid">
        <activity
            android:name=".SettingsActivity"
            android:exported="true"
            android:label="@string/title_activity_settings">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".MainActivity"
            android:exported="true">

        </activity>
    </application>

</manifest>

 

 

- AndroidManifest 에서 intent-filter를 옮겨서 사용자를 처음 대면하는 화면 바꾸기.

 

- 하나의 앱은 여러개의 액티비티로 구성될 수 있는데,

 한 Activity가 외부로부터 intent를 수신해서 화면을 구성해주는 것이라고 생각하면 된다.

 


 

- settingsActivity의 기본화면이다.

 

- Fragment 가 있고, FragmentPreference 를 상속받은 상태이다.

- onCreatePreference 메소드를 override

 

- setPreferenceFromResource로 읽어온다.

 

- res/xml 폴더에 xml 파일이 하나 있는데, 이 activity의 기본 레이아웃이 여기에 들어있다.

 

root_preference.xml

<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">

    <PreferenceCategory app:title="@string/messages_header">

        <EditTextPreference
            app:key="signature"
            app:title="@string/signature_title"
            app:useSimpleSummaryProvider="true" />

        <ListPreference
            app:defaultValue="reply"
            app:entries="@array/reply_entries"
            app:entryValues="@array/reply_values"
            app:key="reply"
            app:title="@string/reply_title"
            app:useSimpleSummaryProvider="true" />

    </PreferenceCategory>

    <PreferenceCategory app:title="@string/sync_header">

        <SwitchPreferenceCompat
            app:key="sync"
            app:title="@string/sync_title" />

        <SwitchPreferenceCompat
            app:dependency="sync"
            app:key="attachment"
            app:summaryOff="@string/attachment_summary_off"
            app:summaryOn="@string/attachment_summary_on"
            app:title="@string/attachment_title" />

    </PreferenceCategory>

</PreferenceScreen>

 

- PreferenceCategory가 2개 있고,

그 안에 EdittextPreference, ListPreference, SwitchPreferenceCompat 이 들어있는 구조

 

- 이 xml문서 안에 들어가 있는 문자열은 하드코딩 된 것이 아니라

  res/values/string.xml 에 있는 문자열을 가져다가 쓰는 것이다.

- string 파일에서 참조했다는 의미로 @string/xxx 로 작성되어 있는 것!

 

- 두 파일의 관계를 잘 알아두기!

 

- 문자열이 이렇게 따로 작성되어 있는 이유는 다국어를 지원하기 위해서이다.

- 지역/location에 따라서 다른 문자열을 작성하고 싶을 때 이렇게 사용한다.

 

 

- strings.xml 에서 수정하면 이렇게 수정된 텍스트가 나온다.

- 내부적으로 이것도 SharedPreference를 사용하는 것이다.

- 여기서 사용하는 정보들을 저장해 두었다가 Activitiy가 활성화될 때 복구시키는 것

 


 

SharedPreferences pref= PreferenceManager.getDefaultSharedPreferences(this);
pref.registerOnSharedPreferenceChangeListener(this);

- Preference 리스너 등록

- SettingsActivity에 SharedPreferences.OnSharedPreferenceChangeListener 상속해주기

- 설정 창에서 무언가에 어떤 값을 넣거나 on/off하거나 ... 그런 수정된 사항을 듣는 리스너이다.

 

- onSharedPreferenceChanged 를 오버라이드

 


 

- Activity 끼리 이동할 수 있도록 연결하기!

 

- onCreateOptionsMenu, onOptionsItemSelected 메소드 2개 override

 

- 옵션 메뉴는 자주 사용해서 리스너를 등록하지 않아도 옵션메뉴 관련된 메소드를 그냥 사용하면 된다.

 


- 옵션 창 만들어주기

- res-new- android resource directory 에서 menu 폴더 생성

 

 

- 메뉴폴더 우클릭-new- new menu resource file

 

menu_main

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/setting"
        android:title="설정"
        app:showAsAction="never"/>
    <item android:title="하나" />
    <item android:title="두울" />
    <item android:title="세엣" />
</menu>

 

- menu item을 드래그해서 4개 만들어줌

 

 

- 여기서 옵션메뉴를 보이도록 설정할 수 있다.

- never 항상 숨겨놓겠다는 뜻!

 

- item에 id 부여하기!

 

 

MainActivity (수정)

package com.example.step13sharedpref

import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager

/*
    App에서 문자열을 영구 저장하는 방법(영구 저장이란 앱을 종료하고 다시 시작해도 불러올 수 있는 문자열)

    1. 파일 입출력을 이용해서 저장
    2. android 내장 DataBase를 이용해서 저장 => QLite DataBase (안드로이드에 기본으로 내장된 프로그램)
    3. SharedPreference를 이용해서 저장 (느리지만 간단히 저장하고 불러올 수 있다)
        내부적으로 xml 문서를 만들어서 문자열을 저장하고 불러온다.
        저장된 문자열을 boolean, int, double, String type 으로 변환해서 불러올 수 있다.
 */
class MainActivity : AppCompatActivity(), View.OnClickListener { //extends AppCompatActivity implements View.OnClickListener
    /*
        java에서는 field 를 선언만 하면 자동으로 null로 초기화된다.
        kotlin 에서는 null이 가능한 field 를 만들어서 명시적으로 넣어주어야 한다.
     */
    var editText:EditText?=null

    // onCreate() 메소드 재정의
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // res/layout/activity_main.xml 문서를 전개해서 화면 구성하기
        setContentView(R.layout.activity_main)

        //EditText 객체의 참조값 얻어오기
        editText=findViewById<EditText>(R.id.editText)
        //Button 객체의 참조값 얻어오기
        //val saveBtn=findViewById<Button>(R.id.saveBtn)
        val saveBtn:Button=findViewById(R.id.saveBtn)
        saveBtn.setOnClickListener(this)
        
        val readBtn=findViewById<Button>(R.id.readBtn)
        /*
        readBtn.setOnClickListener(object:View.OnClickListener {
            override fun onClick(v: View?) {

            }
        })
        */
        
        //위의 코드를 간략히 표현하면 아래와 같다.
        readBtn.setOnClickListener {
            val pref:SharedPreferences = getSharedPreferences("info", Context.MODE_PRIVATE)
            // "msg" 라는 키값으로 저장된 문자열 읽어오기, 없다면 defValue 값이 읽어와진다.
            val msg=pref.getString("msg", "")
            /*
                여기서 this는 MainActivity 객체를 가리킨다.
                java 에서는 익명 클래스 안에서 바깥 클래스 객체의 참조값을 가리키려면
                MainActivity.this 와 같이 클래스명을 명시했어야 한다.

                kotlin 에서도 익명 클래스 안에서 바깥 클래스 객체의 참조값을 가리키려면
                this@MainClass 와 같이 클래스명을 명시하면 된다.

                단, 간략히 표현한 블럭 안에서는 this 만 써도 바깥 클래스 객체를 가리킬 수 있다.
             */
            Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()

        }
    }

    override fun onStart() {
        super.onStart()

        val pref=PreferenceManager.getDefaultSharedPreferences(this)
        //액티비티가 활성화되는 시점에 설정에 저장된 값을 읽어오고 싶다면 여기서 작업하면 된다.
        val signature=pref.getString("signature", "")
        val reply=pref.getString("reply","")
        val sync=pref.getBoolean("sync", false)

        Toast.makeText(this, "signature:${signature}, reply:${reply}, sync:${sync}", Toast.LENGTH_LONG).show()
    }

    //저장 버튼을 누르면 호출되는 메소드
    override fun onClick(v: View?) {
        //EditText에 입력한 문자열 읽어오기
        val msg=editText?.text.toString() //null 이 가능한 변수나 필드의 값을 참조할 때는 ? 가 필요하다.
        //SharedPreferences 의 참조값 얻어오기
        val pref:SharedPreferences=getSharedPreferences("info", Context.MODE_PRIVATE)
        //에디터 객체의 참조값 얻어오기
        val editor:SharedPreferences.Editor=pref.edit()
        //에디터 객체를 이용해서 문자열을 key : value 형태로 영구 저장할 수 있다.
        editor.putString("msg", msg)
        editor.commit()

        AlertDialog.Builder(this)
                .setMessage("저장했습니다.")
                .setNeutralButton("확인", null)
                .create()
                .show()
    }
    //옵션 메뉴를 구성하는 메소드
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        //menu/menu_main.xml 문서를 전개해서 메뉴 구성하기
        //in java => getMenuInflater().inflate()
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    //옵션 아이템을 선택했을 때 호출되는 메소드
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        //선택한 메뉴의 아이디 읽어오기
        val id=item.itemId;
        //만일 설정 메뉴를 선택했다면
        if(id == R.id.setting){
            //kotlin 에서 특정 클래스 type은 "클래스명::class.java" 라고 해야 한다.
            val intent= Intent(this, SettingsActivity::class.java)
            //SettingsActivity 를 시작하겠다는 의도를 가지고 있는 Intent 객체를 이용해서
            //액티비티 시작시키기
            startActivity(intent)
        }

        return true
    }
}

 

 

- 옵션 아이템을 선택하면 이 selected 메소드가 호출되도록

 

- 그러나 코틀린에서는 클래스를 인자로 받는 자리에 .class 라고 쓸 수 없다. 코틀린에서는 다르게 쓴다!

 

val intent= Intent(this, SettingsActivity::class.java)

- 이렇게 작성해준다.

 

- 그러면 이제 메뉴에서 설정을 누르면 settingActivity로 이동한다.

 


 

- 기기에 있는 뒤로 가는 버튼은 back button이다. (빨강)

- 안드로이드에서 이 상단메뉴의 ← 버튼은 up button이라고 불린다. (초록)

 

- 안드로이드에서는 좀 비슷하게 사용되지만 다른 버튼이다.

- back 버튼: 페이지 history상 뒤를 말함

- up 버튼: 페이지의 계층구조상 위로 간다.

 

- 상황에 따라서 이동하는 페이지가 같을 수도 있지만 엄연히 다른 기능이다.

 

 

- 이렇게 세개의 페이지가 있다고 가정하자

- 이메일 버튼(파란색) 클릭시 List로 이동

- 새로운 메일이 도착했다는 메뉴(분홍색) 클릭시 Detail 로 이동

 

 

- List에서 자세히 보기 메뉴(초록색)를 누르면 Detail로 이동하는 흐름도 있을 수 있다.

- 이 초록색 경로의 경우에는 up과 back의 버튼이 효과가 같다.

 

- 그러나 Main 에서 분홍색 화살표로로 이동했을 때에는 두 버튼의 경로가 다르다.

- 이 경우 back버튼과 up버튼이 다른 경로로 이동한다.

 

- history상의 이전으로 가는 버튼과 계층구조상의 상위로 이동하는 버튼을 구분하기!

 

 

AndroidManifest (수정)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyAndroid">
        <activity
            android:name=".SettingsActivity"
            android:exported="true"
            android:label="@string/title_activity_settings"
            android:parentActivityName=".MainActivity">

        </activity>
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 

- parentActivityName : 계층구조상 상위의 액티비티를 명시하는 속성

 

- manifest에서 수정

- <activity> 요소의 속성으로 parentActivityName 을 ".MainActivity"로 작성

- 설정으로 이동해서 upbutton을 사용하면 MainActivity로 가게 된다.

 

- 설정한 후에 SettingActivity화면에서 up 버튼을 누르면 Main으로 돌아온다.

- 이렇게 설정해두면 어디에서든 up만 누르면 메인으로 갈 수 있다.(계층 상위로 이동)

- 이 내용을 AndroidManifest.xml에서 구성한다.

 

- SettingsActivity의 이 코드가 상단의 up버튼이 나타나는 부분이다.

 


 

- 파일시스템에서 우클릭-Synchronize. 동기화 버튼(refresh 비슷)

 

- xml파일에 들어가서 보면 이렇게 저장되어 있다.

 

- 화면에서 설정된 내용이 자동으로 수정되어 기기에 저장되게 되어 있다.

 

- 어떻게 이런 페이지가 구성되는지? 그리고 읽어오는 방법은?

- 이 내용을 익히면 설정에 관련된 액티비티를 편리하게 구성할 수 있다.

 

- 이 화면은 Fragment이다.

- fragment와 자동으로 연동되게 되어있다.

 

 

settings_activity.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/settings"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

 

 

- settings_activity.xml 파일에 들어가보면 FrameLayout 위에 설정 프래그먼트를 띄운 것을 확인 가능!

 

- 액티비티에서 코드를 보면 SettingsFragment를 이 FrameLayout 위에 띄운 형태이다.

- SettingsFragment는 아래에 내부클래스로 정의되어 있다.

- root_preferences.xml 문서를 전개해서 설정 메뉴를 구성한다.

 


 

 

- root_preferences.xml 수정

 

- 이 하나하나가 preference 카테고리이다.

- 메세지, 동기화 등의 제목이 title 이다.

 

EditTextPreference

- 문자열을 입력할 수 있는 창

- 세번째 줄의 useSimpleSummaryProvider 는 길게 작성되었을 경우 요약본을 제공할 것인지 여부!

 

ListPreference

- 선택지를 선택하게 만드는 창

- 이 창에서 보여줄 후보군이 entries

- entries에 들어갈 내용은 reply/values/arrays.xml 안에 들어있다.

 

arrays.xml

- 사용자에게 보여주는 값과 실제로 저장되는 value가 나누어져 있다.

- html의 select option을 구성할 때와 같다.

 

SwitchPreferenceCompat

- 스위치 on/off 버튼

 

- 위 스위치가 꺼지면 아래는 아예 선택 불가하도록 설정되어 있다.

- 아래 있는 스위치는 위에 있는 스위치에 의존성을 가지고 있다는 뜻.

 

app:dependency="sync"

- 코드에서는 dependency 가 의존성을 갖는 속성이다.

 

 

- 각각 off 했을 때 출력하는 문자열, on 했을 때 출력하는 문자열이다.

 

- 이것은 눈으로 보이는 UI일 뿐, 앱에서 여기에 있는 정보를 실제로 활용하려면?

- 참조값을 받아와서 SettingsActivity에서 작성

 


 

@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {

}

- key 값으로 얻어온다.

 

- 키 값은 지금 이렇게 정해져 있다. 마음대로 정할 수 있다!

 

- 옵션의 키 값에 따라 if~else if로 3가지 선택지로 나누어준다.

- sync 는 문자열이 전달되는 것이 아니라 T/F가 전달되므로(boolean 타입),

 getString() 이 아닌 getBoolean() 으로 가져와주기

 

 

- 이제 옵션의 값을 바꾸면 해당 내용을 담은 Toast 메시지가 뜬다.

 


 

- 변경했을 때 변경된 내용이 아니라 저장된 값을 읽어오고 싶은 경우에는?

- 다른 액티비티에서 그 값을 사용한다든가, 초기화하면서 다른동작을 준비하는 등...

  다른 과정에 여기서 저장된 값이 필요할 수도 있다

 

- abcd 라고 값을 입력하다가 설정으로 이동했다가 다시 돌아왔을때, abcd는 살아있다.

→ MainActivity는 파괴된 것이 아니고 onStop() 단계에 머물러 있다.

 

- upButton은 onCreate부터 시작한다.

 

- MainActivity에서 onStart() 오버라이드. 

- 사용자가 입력한 저장된 값을 읽어오기!

 

getDefaultSharedPreferences

- 이것으로 preference의 참조값을 얻어와서 작업할 수 있다.

 

- 이제 MainActivity로 이동할 때 토스트 메시지에 현재 저장된 각각의 설정값이 뜬다.

- up 버튼을 사용하든 back 버튼을 사용하든 뜬다. MainActivity의 onStart가 기준이다.

 

- 필요한 값을 원하는 시점에 읽어올 수 있도록 연습해보기

 

ex) 설정옵션 유형 설정하기

- 이름,취미 등 : edittextPreference (String 값 반환)

- 성별 등 : switch버튼 또는 둘 중 하나 선택 하는 창 (boolean 또는 if/else 로 리턴값 분기)