Preference

17 분 소요

예제 설명

본 예제에서는 음악에 따라 움직이는 visualizer activity가 하나 있는 토이 앱을 통해 SharedPreference를 이용한 setting창을 만들어 본다. visualizer activity를 구성하는 VisualizerView는 커스텀으로 만들어져 있다.

안드로이드 데이터 저장 방법 5가지 (Data Persistence)

저장 옵션
저장되는 데이터의 Type
Data가 유지되는 시간의 범위
onSavedInstanceState
key/value (complex values)
앱이 열려있는 동안만 유지
SharedPreference
key/value(primitive values)
앱과 폰이 재시작되어도 유지
SQLite Database
많은 양의 정제된 텍스트/숫자/boolean
앱과 폰이 재시작되어도 유지
Internal Storage
Multimedia or lager data
앱과 폰이 재시작되어도 유지
Server
서로다른 디바이스에서 접근가능한 데이터
앱과 폰이 재시작되어도 유지/다른 폰에서도 접근 가능

이번 글에서 다룰 것은 당연히 SharedPreference이다.

fragment를 이용한 preference 생성하기

preference fragment는 일반적인 fragment와는 생성방식이 다르다.

preference fragment를 이용하기 위해서는 먼저 v7 지원 라이브러리의 Preference를 dependency에 추가해 주어야 한다. 참고 공홈 자료 클릭

아래와 같이 gradle파일에 추가하자.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:25.1.0'
    implementation 'com.android.support:preference-v7:25.2.0' // 이부분 추가
}

이제 SettingFragment 를 만들어 보자. 먼저 Class를 만든다. PreferenceFragmentCompat을 상속받도록 하자. 그럼 onCreatePreference()를 반드시 Override해야 한다.

import android.os.Bundle;
import android.support.v7.preference.PreferenceFragmentCompat;

public class SettingFragment extends PreferenceFragmentCompat {
    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        
    }
}

이제 이 Fragment안에 들어갈 내용물인 xml을 만들자. res밑에 xml폴더를 만들고, 그 안에 .xml파일을 만들자. 이름은 pref_visualizer.xml로 해 보자. 그럼 아래와 같이 태그로 둘러싸인 .xml파일이 만들어질 것이다. (세팅 전용 linear layout이라 생각하자)

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

</PreferenceScreen>

어떤 .xml파일이든지간에 이 태그로 둘러싸여야 한다. 태그 안에 nested 도 만들 수 있다. 어떤 앱은 세팅의 특정 값을 클릭할 경우 또다른 세팅이 열리는 경우가 있는데, 바로 nested 를 이용한 것.

일단 먼저 간단한 checkbox preference를 추가해 보자.

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <CheckBoxPreference
        android:defaultValue="true" //기본값 
        android:key="show_bass" // class에서 식별할 key값
        android:summaryOff="Hidden" // title 밑에 나오는 작은 설명 (off일때)
        android:summaryOn="Shown" // title 밑에 나오는 작은 설명 (on일때)
        android:title="Show Bass"/> // preference명
</PreferenceScreen>

이제 xml파일을 저장한 후 아까 만들어 놓은 onCreatePreference()에 추가해 보자.

 ...
 @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        addPreferencesFromResource(R.xml.pref_visualizer);
    }
...

이제 이 fragment를 원하는 layout에 추가해 주면 된다. activity_setting.xml 에 아래와 같이 추가해 보자.

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
          android:id="@+id/activity_settings"
          android:name="android.example.com.visualizerpreferences.SettingFragment" // 앱이름과 fragment이름에 따라 달라짐 
          android:layout_width="match_parent"
          android:layout_height="match_parent"/>

마지막으로 style파일에 preference theme을 지정해 주어야 한다. 이걸 안 해주면 앱이 크러쉬 난다는 거…

<resources>

    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="preferenceTheme">@style/PreferenceThemeOverlay</item> //이 부분
    </style>

</resources>

이제 이 fragment가 속할 activity를 만들고 넣어준다. 즉 settingActivity.java-activity_setting.xml로 연결되어 있고, activity_setting.xml안에 하나를 넣어 주고 여기에 SettingsFragement.java를 연결하면 된다.

preference 설정에 따른 동작

현재까지 진행상황은

  1. 세팅 액티비티 만들기
  2. Preference Fragment 추가
  3. Preference로 하여금 UI변경
  4. Preference Summary 추가하기

에서 2단계까지 한 것이다. 이제 실제로 Setting에서 건드린 값을 액티비티에 어떻게 적용시키는 지 알아보자.

현재 기본 Setting은 아래와 같다.

        @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_visualizer);
        mVisualizerView = (VisualizerView) findViewById(R.id.activity_visualizer);
        defaultSetup();
        setupPermissions();
    }

    // vizualizer activity에 있는 setup method
    private void defaultSetup() {
        mVisualizerView.setShowBass(true); //현재는 setting값 변경에 관계없이 true로 설정됨
        mVisualizerView.setShowMid(true);
        mVisualizerView.setShowTreble(true);
        mVisualizerView.setMinSizeScale(1);
        mVisualizerView.setColor(getString(R.string.pref_color_red_value));
    }

위에서 bass를 보일지 말지에 대한 setting을 추가했으므로 setShowBass메쏘드를 변경해야 한다. 이를 위해서 SharedPreference instace를 하나 생성해야 하는데, Preference매니저의 getDefaultSharedPreference 를 이용한다. 동일 context에 두개 이상의 preference가 있을 경우 getSharedPreference를 이용한다. 자세한 건 –>여기클릭

그래서 일단 위 메쏘드 이름을 setupSharePreference로 바꾸고, 그 안에서 해당 작업을 하자.

먼저 SharedPreferences의 인스턴스를 만든다.

    private void setupSharedPreferences() {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        ...
      }

sharedPreference 는 데이터형에 따른 다양한 set/get method를 가지고 있다. 여기서는 getBoolean(키값, 디폴트값)을 통해 우리가 setting Preference에서 선택한 값을 불러서 setShowBass Method에 적용시키자.

        ...
        mVisualizerView.setShowBass(sharedPreferences.getBoolean("show_bass",true));
        ...

이제 값을 가져오기는 했는데, 문제는 이 setup 메쏘드가 onCreate() 안에 있기 때문에, 화면을 로테이팅 시킨다거나 하는 onCreate() 메쏘드가 적용되지 않을 때에는 실시간으로 적용되지 않는다는 점이다.. 사실 menifesto의 visualizerActivity의 런치모드가 android:launchMode="singleTop"으로 되있기 때문이라 단지 onPuase() > onResume()되기 때문이다. 이거를 건드리면 또 매번 onCreate()를 불러오기 때문에 낭비가 심하다. 따라서 이 문제는 어떻게 해결할 것인가?

BEST PRACTICE - String을 Resource에서 불러오기

위에 대한 해답을 얻기 전에 잠깐 수정할 부분이 있다. 지금까지 예제에서는 빠른 진행을 위해 하드코딩된 String값을 많이 이용했다. 실제로 개발자들이 현직에서 할 때는 거의 모든 부분에 Resource를 이용한다(boolean값까지) 이를 먼저 적용시키고 넘어가자.

  1. res/values/String.xml
    <String name ="pref_show_bass_lable">Show Bass</String>
    <String name ="pref_show_bass_key" translatable="false">show_bass</String> //키값은 번역하지 않는다. 사용자한테 보이지 않기 때문. 
    
    <!--Summary for checkbox -->
    <String name ="pref_shown">Shown</String>
    <String name ="pref_hidden">Hidden</String>

두번째로 boolean값을 저장하기 위해 새롭게 res폴더를 우클릭하고 bools라는 이름의 새 xml파일을 만들자. 타입이 value이면 자동으로 values폴더 밑에 들어간다.

  1. res/values/bools.xml

아래와 같이 show bass를 위한 디폴트 값을 true로 지정하자.

<resources>
    <bool name="pref_show_bass_default">true</bool>
</resources>

위의 두 개를 했으면 이제 pref_visualizer의 값을 아래와 같이 대체할 수 있다.

<CheckBoxPreference
    android:defaultValue="@bool/pref_show_bass_default"
    android:key="@string/pref_show_bass_key"
    android:summaryOff="@string/pref_show_false"
    android:summaryOn="@string/pref_show_true"
    android:title="@string/pref_show_bass_label" />

또한 VizualizerActivity.java내에서도 아래와 같이 바꿔준다.

...
  mVisualizerView.setShowBass(sharedPreferences.getBoolean(
    getString(R.string.pref_show_bass_key), 
    getResources().getBoolean(R.bool.pref_show_bass_default)));
...

getString(리소스) , getResource().getBoolean(리소스)

OnSharedPreferenceListener

이제 preference변경에 대한 반영을 onCreate()을 통하지 않고도 실시간으로 반영하는 방법에 대해 알아보자. 바로 OnSharedPreferenceListener를 통해서 가능하다.

리스너를 적용시키는 데에는 4가지 스텝이 있다.

  1. 본래의 액티비티에서 OnSharedPreferenceListener를 implement하기
  2. onSharedPreferenceChanged() 오버라이드
  3. onCreate()시 뷰에 리스너 등록하기
  4. onDestroy()에 리스너 해지하기

(1)먼저 vizualizerActivity에 implement시켜준다.

public class VisualizerActivity extends AppCompatActivity
        implements SharedPreferences.OnSharedPreferenceChangeListener {
...

(2)그러면 onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)를 오버라이드해야 한다. 이 메쏘드 안에서 해야 할 일은 sharedPreference와 key값이 각각 parameter로 들어가 있다. key parameter가 show_bass_key값일 경우 setShowbass()를 통해 적용시키는 작업을 진행한다.

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        if (key.equals(getString(R.string.pref_show_bass_key))) {
            mVisualizerView.setShowBass(sharedPreferences.getBoolean(key,
                    getResources().getBoolean(R.bool.pref_show_bass_default)));
        }
    }

(3) activity에 리스너를 등록한다. 전체 activity가 OnSharedPreferenceListener를 implement하고 있으므로 this를 통해 등록하면 된다.

...
        sharedPreferences.registerOnSharedPreferenceChangeListener(this);
...

(4)마지막으로 생명주기 종료 시 리스너 등록을 해지한다.

    @Override
    protected void onDestroy() {
        super.onDestroy();
        PreferenceManager.getDefaultSharedPreferences(this)
                .unregisterOnSharedPreferenceChangeListener(this);
        //굳이 instance생성 필요 없음
    }

checkbox preference 추가하기

이제 middle 및 treble에 대해서도 bass와 같이 show 옵션을 컨트롤할 수 있도록 추가하자.

  1. string.xml/bool.xml 에 값 추가
  2. pref_visualizer.xml에 CheckboxPreference 2개 추가
  3. activity 코드에 setupdefault() 및 onSharedPreferenceChanged()에 middle및 treble 추가

List Preference

여러 개의 선택지 중 하나를 선택하는 옵션을 list preference라고 한다.

pref_vizualizer.xml파일에 객체를 추가하자. 이 때 entries와 entryValues 값은 array객체를 지정해 주어야 한다.

    <ListPreference
        android:defaultValue="@string/pref_color_red_value"
        //새롭게 array객체 필요 
        android:entries="@array/pref_color_option_labels" 
        android:entryValues="@array/pref_color_option_values"
        // 
        android:key="@string/pref_color_key"
        android:title="@string/pref_color_label" />

이를 위해 res 밑에 arrays.xml resource파일을 만들고 아래와 같이 추가한다. 이 때 주의해야 할 점은 어떤 label이 어떤 value와 매칭되는지는 전적으로 작성 순서에 의존한다. 따라서 아래와 같이 red-blue-green이라면 그 순서를 맞춰주는 게 중요하다.

    // 레이블 어레이 (사용자에게 보이는)
    <array name="pref_color_option_labels">
        <item>@string/pref_color_red_label</item>
        <item>@string/pref_color_blue_label</item>
        <item>@string/pref_color_green_label</item>
    </array>
    
    // 값 어레이 
    <array name="pref_color_option_values">
        <item>@string/pref_color_red_value</item>
        <item>@string/pref_color_blue_value</item>
        <item>@string/pref_color_green_value</item>
    </array>

나머지 과정은 checkBox preference과정과 똑같다.

ListPreference 에 Summary추가하기

CheckBoxPreference 와는 다르게 ListPreference에는 summary가 나오지 않는다. 이유는 무엇인가? CheckBoxPreference객체에는 xml attribute 중 summaryOff/summaryOn attribute로 summary를 달아주었기 때문..ListPreference객체에는 해당 attribute이 없기 때문에 직접 코딩을 통해 달아줘 보자.

먼저 SettingFragment.java로 가서 onCreatePreferences() 안에 SharedPreferece객체 및 PreferenceScreen객체의 인스턴스를 생성하자.


public class SettingsFragment extends PreferenceFragmentCompat {

    public void onCreatePreferences(Bundle bundle, String s) {

        addPreferencesFromResource(R.xml.pref_visualizer);

        // PreferenceFragment의 자식 fragment이기 때문에 앞전 activity에서 sharedPreference를 부르는 것과는 다른 메소드를 사용한다. 
        // preferenceScreen에서 getSharedPreference 이용. 
        SharedPreferences sharedPreferences = getPreferenceScreen().getSharedPreferences();
        //PreferenceScreen은 가장 root객체이다. pref_vizualizer.xml에 가 보면 가장 부모 객체가 <PreferenceScreen>임을 알 수 있다. 
        PreferenceScreen preferenceScreen = getPreferenceScreen();
    }

이제 Summary를 달아주는 helper method를 생성하자. 왜냐? 프레그먼트 생성시(onCreatePreferences)와 리스너(onSharedPreferenceChanged)에 각각 아래 내용이 들어가야 하므로 작성해서 사용하면 편리하다.

    /**
     * ListPreference일 경우 Summary를 추가하는 Method
     *
     * @param preference 업데이트 될 preference
     * @param value Label 을 찾기위한 value
     */
    public void setPreferenceSummary(Preference preference, String value) {
        //xml파일과 비교해 보자. preference는 하나의 xml객체다..즉 pref_vizualizer.xml 에 있는 객체들인 것. 
        // parameter로 투입된 preference객체가 <listPreference> 일 경우 수행 
        if(preference instanceof ListPreference) {
            // listPreference로 캐스팅하고 
            ListPreference listPreference = (ListPreference) preference;
            // 같이 넣어준 value값을 통해 index(순서)를 구한다. 
            int index = listPreference.findIndexOfValue(value);
            // 순서가 valid할 경우 
            if(index >= 0) {
                // setSummary() 메쏘드를 통해 summary를 달아준다. 
                // getEntries는 Label Array를 구하는 메쏘드. 즉 레이블을 달아준 것.  
                listPreference.setSummary(listPreference.getEntries()[index]);
            }
        }
    }

이제 다시 onCreatePreferences로 돌아가서..

        //preferenceScreen에 preference객체가 몇 개 있는지 알아내고 
        int count = preferenceScreen.getPreferenceCount();
        // 전체 객체에 대해 조치를 취한다...
        for (int i = 0; i < count; i++) {
            Preference p = preferenceScreen.getPreference(i);
            // <CheckboxPreference>가 아닐 경우에만!
            if(!(p instanceof CheckBoxPreference)){
                //위에 작성해 놓은 setPreferenceSummary헬퍼 메쏘드를 쓰기 위해 preference및 value값을 알아내자. preference 는 현재 p고, 

                // value는 sharedPreferences.getString(키값, 디폴트값)을 통해 알아낸다. 
                // 이 때, 위에서 ChechBoxPreference를 제외시키지 않았다면 runtime error가 발생할 수 있다. 왜냐면 ChechBoxPreference의.getKey를 하면 Bool값이 나올것이니깐..
                String value = sharedPreferences.getString(p.getKey(),"");
                
                // Summary를 달아준다.
                setPreferenceSummary(p, value);
            }
        }

여기까지 하고 앱을 실행시키면 Summary가 달린 게 보일 것이다. 근데…실시간으로 반영은 안 된다!!

PreferenceFragment 에 Listner달아주기

실시간 반영을 위해 vizualizer activity에서 했던 것처럼 여기도 OnSharedPreferenceChangeListener를 달아주자.

방법은 거의 동일하다. 먼저 OnSharedPreferenceChangeListener를 implement한다. 이후 onSharedPreferenceChanged를 아래와 같이 override하자.

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        
        //어떤 preference인지는 굳이 PreferenceScreen불러내지 않고 주어진 key로부터 뽑아낼 수도 있다. 
        Preference p = findPreference(key);
        if(!(p instanceof CheckBoxPreference)){
            String value = sharedPreferences.getString(p.getKey(),"");
            setPreferenceSummary(p, value);
        }
    }

이후 fragment생성 및 소멸 시 리스너를 등록/해제해주면 된다.

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getPreferenceScreen().getSharedPreferences()
                .registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        getPreferenceScreen().getSharedPreferences()
                .unregisterOnSharedPreferenceChangeListener(this);
    }

EditText Preference 추가하기

EditText는 직접 입력할 수 있는 값을 받아준다. ListPreference와 같이 제한된 범위 내에서 선택할 수 없을 때 사용한다. 이 예제에서는 도형의 크기를 실수(float)로 받아주기 위해 사용했다.

마찬가지로 pref_visualizer.xml에 아래와 같이 추가하자.

    //사용할 string값은 미리 만들어 두자. 
    <EditTextPreference
        android:defaultValue="@string/pref_size_default" // 1
        android:key="@string/pref_size_key"
        android:title="@string/pref_size_label"
        />

VizualizerActivity.java에 아래와 같이 helper method를 추가한다.

...
    private void loadSizeFromPreferences(SharedPreferences sharedPreferences) {
        //실수값을 받을 수 있도록 float 선언 
        //string값을 float으로 변환하기 위한 Float.parseFloat사용
        float size = Float.parseFloat(sharedPreferences.getString(getString(R.string.pref_size_key),
                getResources().getString(R.string.pref_size_default)));
        mVisualizerView.setMinSizeScale(size);
    }
...

마지막으로 SettingFragment.java에 setPreferenceSummary()에 EditTextPreference에 대한 summary를 추가해 주면 완료.

수용 범위 설정 (preferenceChangeListener)

방금 설정한 EditTextPreference에는 큰 문제점이 있는데.. 사용자가 직접 입력하기 때문에 그 입력값이 예상치 못할 경우 crush가 난다. 당장 위에 보면 실수 값을 받는다고 가정하고 Float.parseFloat() 을 썼는데, 여기 에 알파벳이 들어간 경우 바로 crush가 난다. 특히 문제인 것은 sharedPreference는 앱이 종료된 이후에도 저장되는 값이기 때문에, 한번 잘못 입력하게 되면 다음에 앱을 실행시켜도 계속 꺼진다는 거…(..)

그렇기 때문에 입력값이 sharedPreference에 저장되기 전에 감시할 리스너가 필요하다. 이것이 바로 PreferenceChangeListener이다.

SharedPreferenceChangeListenerPreferenceChangeListener의 차이점

  • SharedPreferenceChangeListener는 어떤 값이 SharedPreference file에 저장되면 발동된다.
  • PreferenceChangeListener는 어떤 값이 SharedPreference File에 저장되기 전에 발동된다. 그렇기 때문에 불가능한 값이 저장되기 전에 방지가 가능
  • PreferenceChangeListener는 하나의 preference에 부착 가능하다. (SharedPreferenceChangeListener는 전체적으로 부착)

그래서 일반적으로 입력 시에는 순서가 아래와 같다.

  1. 사용자가 하나의 preference를 업데이트 하면
  2. PreferenceChangeListener가 해당 preference에 대해 발동
  3. (true가 return되면) 새로운 값이 SharedPreference File에 씌여짐
  4. onSharedPreferenceChanged 리스너가 발동

그 이외의 부분에서는 거의 비슷하게 동작한다. 액티비티에서 Preference.OnPreferenceChangeListener 를 implement하면, onPreferenceChange(Preference preference, Object newValue)를 override해야 하고, onPreferenceChange 메쏘드가 true또는 false를 반환할 것이다. 이 때 false값이 반환되면 값이 씌여지지 않는다.

그럼 진행해보도록 하자.

SettingsFragment가 Preference.OnPreferenceChangeListener 를 implement한다

public class SettingsFragment extends PreferenceFragmentCompat implements
        OnSharedPreferenceChangeListener, Preference.OnPreferenceChangeListener {
...

onPreferenceChange를 override

    @Override
    public boolean onPreferenceChange(Preference preference, Object newValue) {
        // 에러발생시 토스트를 미리 만들어 두고
        Toast error = Toast.makeText(getContext(),"Please summit float 0.1 to 3",Toast.LENGTH_LONG);
        // 새 값을 float처리하여 0.1과 3 사이에 있을 때에만 true를 반환하자 
        try {
            float size = Float.parseFloat(newValue.toString());
            if(size > 3 || size <= 0.1) {
                error.show();
                return false;
            }
        } catch (NumberFormatException e) {
            error.show();
            return false;
        }
        return true;
    }

마지막으로 onCreatePreference에서 key값으로 EdittextPreference를 찾고 리스너를 달아주면 끝

    @Override
    public void onCreatePreferences(Bundle bundle, String s) {
        
        ...
        //findPreference 를 이용해 찾자 
        Preference preference = findPreference(getString(R.string.pref_size_key));
        preference.setOnPreferenceChangeListener(this);
    }

Setting으로 만들 값들을 선택하는 방법(material guide)

구글 Material Guide에서는 Setting의 조건과 디자인에 대해 설명해 주고 있다. 사실 사용자에게 너무 많은 선택지를 주면 오히려 사용자는 혼란을 느끼게 된다. 또한 세팅으로 해야 할 것들이 있고 아닌 것들이 있다. 링크를 통해 확인하자. 링크

댓글남기기