본문 바로가기
깡샘 코틀린

17-3 공유된 프리퍼런스에 보관하기

by 농농씨 2023. 7. 12.

공유된 프리퍼런스 이용하기

앱의 데이터를 내부 저장소에 저장하려면 앞에서 살펴본 데이터베이스나 파일을 이용할 수도 있지만 공유된 프리퍼런스(SharedPreferemces)를 사용하는 방법도 있다. 공유된 프리퍼런스는 플랫폼 API에서 제공하는 클래스로, 데이터를 키-값 형태로 저장할 때 사용한다. 공유된 프리퍼런스는 앱의 간단한 데이터를 저장하는 데 유용하며 내부적으로 내장 메모리의 앱 폴더에 XML 파일로 저장된다.

SharedPreferences 객체를 얻는 방법은 다음 2가지를 제공한다.

  • Activity.getPreferences(int mode)
  • Context.getSharedPreferences(String name, int mode)

Activity.getPreferences() 함수는 액티비티 단위로 데이터를 저장할 때 사용한다. 이 함수의 매개변수에는 파일명이 들어가지 않으며, 이 함수를 호출한 액티비티 클래스명으로 XML 파일이 자동으로 만들어진다. 예를 들어 MainActivity라는 이름의 클래스에서 이 함수를 이용해 SharedPreferences 객체를 얻었다면 MainActivity.xml 파일이 생성되고 이곳에 데이터가 저장된다.

// 액티비티의 데이터 저장
val sharedPref = getPreferences(Context.MODE_PRIVATE)

또는 앱 전체의 데이터를 키-값 형태로 저장하려고 SharedPreferences 객체를 얻을 때는 Context.getSharedPreferences() 함수를 이용한다. 이 함수의 첫 번째 매개변수에 지정한 이름의 파일로 데이터가 저장된다.

// 앱 전체의 데이터 저장
val sharedPref = getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
// 앱 전체의 데이터가, 첫번째 매개변수에 지정된 이름의 파일로 저장됨

 

공유된 프리퍼런스를 이용해 데이터를 저장하려면 다음과 같은 SharedPreferences.Editor 클래스의 함수를 이용해야 한다.

  • putBoolean(String key, boolean value)
  • putInt(String key, int value)
  • putFloat(String key, float value)
  • putLong(String key, long value)
  • putString(String key, String value)

 

SharedPreferences.Editor 객체는 SharedPreferences의 edit() 함수로 얻는다. 그리고 Editor 객체의 put~으로 시작하는 함수를 이용해 데이터를 담으면 commit() 함수를 호출하는 순간 저장된다.

// 프리퍼런스에 데이터 저장
sharedPref.edit().run { edit() 함수로 SharedPreferences 클래스의 Editor 객체 얻음
    putString("data1", "hello") // Editor 객체의 put~으로 시작하는 함수로 데이터 담음
    putInt("data2", 10)
    commit() // commit() 함수 호출하면서 담았던 데이터를 저장함
}

 

프리퍼런스에 저장된 데이터를 가져오려면 SharedPreferences의 게터 함수를 이용하면 된다.

  • getBoolean(String key, boolean defValue)
  • getFloat(String key, float defValue)
  • getInt(String key, int defValue)
  • getLong(String key, long defValue)
  • getString(String key, string defValue)
// 프리퍼런스에서 데이터 가져오기
val data1 = sharedPref.getString("data1", "world") // 키, 값(String 타입)
val data2 = sharedPref.getInt("data2", 10) // 키, 값(Int 타입)

 

 

앱 설정 화면 만들기

대부분 앱은 여러 가지 사용 환경을 설정하는 기능을 제공한다. 앱의 설정 화면은 액티비티와 사용자 이벤트 처리 그리고 공유된 프리퍼런스 등을 이용해서 구현하지만 화면이나 설정한 데이터를 저장하는 형태는 거의 비슷하다. 따라서 많은 앱에서는 설정 화면을 자동으로 만들어 주는 API를 이용한다. 이 API를 이용하면 개발자가 설정 항목을 정의한 XML만 만들어서 적용하면 된다. 그러면 설정 화면과 사용자 이벤트, 데이터 저장까지 자동으로 구현된다.

플랫폼 API에서 이처럼 앱의 설정 기능을 자동화해주는 API는 많았지만 안드로이드 10버전(API 레벨 29) 부터 모두 deprecated되었다. 그리고 Androidx의 Preference를 이용할 것을 권장하고 있다. AndroidX의 Preference는 앱에서 설정 기능을 제공할 때 이용하는 제트팩의 API이다.

 

AndroidX의 Preference를 이용하려면 빌드 그래들 파일에 다음과 같은 라이브러리를 dependencies로 선언해야 한다.

// AndroidX의 프리퍼런스 사용 선언
implementation('androidx.preference:preference:1.2.0') {
    exclude group: 'androidx.lifecycle', module:'lifecycle-viewmodel'
    exclude group: 'androidx.lifecycle', module:'lifecycle-vidwmodel-ktx'
}

다른 라이브러리 등록과 다르게 프리퍼런스(preference)를 등록할 때는 exclude를 이용해 lifecycle-viewmodel, lifecycle-viewmodel-ktx를 제외해야 한다. appcompat 라이브러리와 충돌 문제로 제외하는 것이다. appcompat 라이브러리 1.5x 버전 이상을 이용할 때를 제외하지 않으면 빌드 시 다음 오류가 발생한다.

Caused by: org.gradle.workers.internal.DefaultWorkerExecutor$WorkExecutionException: A failure
occured while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable

 

프리퍼런스 이용 방법

프리퍼런스를 이용해 앱에 설정 기능을 제공하려면 가장 먼저 res/xml 디렉터리에 설정과 관련된 XML 파일을 만들어야 한다. 설정 XML 파일은 루트 태그가 <PreferenceScreen>이어야 한다. 그리고 이 태그 하위에 <SwitchPreferenceCompat>, <Preference> 등의 태그를 이용해 설정 항목을 준비한다.

// 설정 XML 파일
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
// XML 파일 만들기, 루트태그는 <PreferenceScreen>
    <SwitchPreferenceCompat // 설정항목 중 하나
        app:key="notifications"
        app:title="Enable message notifications" />
    <Preference // 설정항목 중 하나
        app:key="feedback"
        app:title="Send feedback"
        app:summary="Report technical issues or suggest new features" />
</PreferenceScreen>

사용자가 설정 화면에서 설정한 값은 내부적으로 공유된 프리퍼런스를 이용해 키-값 형태로 저장된다. 이때 각 설정 항목의 key 속성값이 데이터의 키가 된다. 예를 들어 사용자가 <SwitchPreferenceCompat> 태그에 해당하는 설정 항목을 enable로 지정해다면 자동으로 저장되는 데이터는 notifications을 키로 하고 값은 true가 된다. 그리고 title 속성은 설정 화면에 출력되는 문자열이다. 

이렇게 만든 XML 파일을 코드에서 적용해야 하는데 이때 PreferenceFragmentCompat 클래스를 이용한다. 즉, PreferenceFragmentCompat을 상속받은 프래그먼트로 설정 화면을 준비한다.

// 설정 XML 파일 적용
class MySettingFragment : PreferenceFragmentCompat() {
// 앞서 만든 설정 관련 XML 파일을 적용하기 위해 PreferenceFragmentCompat 클래스를 상속받은 프래그먼트로
// 설정 화면을 준비함
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
    // 그 프래그먼트 클래스는 함수를 재정의하고, 이 함수에서
        setPreferencesFromResource(R.xml.settings, rootKey)
        // 앞에서 만든 설정 XML을 전달한다
    }
}

preferenceFragmentCompat을 상속받은 프래그먼트 클래스는 onCreatePreferences() 함수를 재정의해서 작성하며, 이 함수에서 setPreferencesFromResource()를 이용해 앞에서 만든 설정 XML 파일을 전달한다.

 

이제 이 프래그먼트를 액티비티에서 출력하면 되는데 이는 일반적인 액티비티에서 프래그먼트를 이용하는 방법과 차이가 없다.

// 액티비티에서 프래그먼트 출력
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="math_parent"
    class="com.example.test17.MySettingFragment" /> 
    // 설정 XML 파일을 전달한 프래그먼트를 액티비티에서 출력

이로써 설정 XML 파일을 참조해 설정 화면이 나오고 사용자가 설정한 내용이 자동으로 저장된다.

 

설정 화면 구성

설정 항목이 많으면 관련 있는 것끼리 묶거나 설정 화면을 여러 개로 나눌 수도 있다. 이때에 <PreferenceCategory>와 <Preference> 태그를 사용한다. 먼저 <PreferenceCategory> 태그를 이용하면 한 화면에 보이는 항목끼리 구분지어 출력할 수 있다.

// 항목끼리 묶기
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
// 설정 XML 파일은 루트 태그가 <PreferenceScreen>이어야 함
    <PreferenceCategory // 한 화면에 보이는 항목끼리 구분지어 출력하기 위한 태그
        app:key="a_category"
        app:title="A Setting">
        <SwitchPreferenceCompat
            app:key="a1"
            app:title="A - 1 Setting" />
        <SwitchPreferenceCompat
            app:key="a2"
            app:title="A - 2 Setting" />
    </PreferenceCategory>
    <PreferenceCategory>
        app:key="B_category"
        app:title="B setting"
        <SwitchPreferenceCompat
            app:key="b1"
            app:title="B - 1 Setting" />
    </PreferenceCategory>
</PreferenceScreen>

 

설정 항목이 더 많을 때는 화면을 여러 개로 분리하는 방법도 있다. 예를 들어 A와 B라는 설정 화면으로 분리한다면 설정 XML과 프래그먼트를 1️⃣2개씩 만들어야 한다. 그리고 이 A, B 설정 화면을 포함하는 2️⃣메인 설정 XML을 작성한다. 메인 설정 XML에서 각 설정 화면은 <Preference> 태그로 지정한다. 그러면 메인 설정 화면에서 사용자가 항목을 클릭할 때 fragment 속성에 지정한 설정 화면으로 전환한다.

// 두 화면을 포함하는 설정 화면
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
    <Preference // 분리된 각 설정 화면을 위한 태그
        app:key="a"
        app:summary="A Setting summary"
        app:title="A setting"
        app:fragment="com.example.test17.ASettingFragment" />
        // 사용자가 각 항목을 클릭할 때 여기 지정된 설정 화면으로 전환됨
    <Preference
        app:key="b"
        app:summary="B Settinng summary"
        app:title="B Setting"
        app:fragment="com.example.test17.BSettingFragment" />
</PreferenceScreen>

 

<Preference> 태그를 이용해 설정 화면을 분할했다면 액티비티에서 PreferenceFragmentCompat.OnPreferenceStartFragmentCallback 1️⃣인터페이스를 구현하고 onPreferencestartFragment() 2️⃣함수를 재정의해서 작성해야 한다. 이 함수를 정의하지 않아도 설정 화면은 분할되지만 뒤로가기 버튼을 눌렀을 때 이전 설정 화면이 나오지 않는 문제가 발생한다.

 

또한 설정 화면이 바뀔 때마다 액티비티의 액션바에 출력되는 제목을 바꿀 수도 있다. onPreferenceStartFragment()는 설정 화면이 바뀔 때마다 호출되는 함수이므로 이 함수에 작성하면 된다.

// 분할 설정 화면을 보여주는 액티비티 코드
class SettingActivitiy : AppCompatActivity(),
    PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
    // 설정화면 분할 위한 인터페이스를 구현함
    (... 생략 ...)
    override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat,
    // 설정화면 분할했을 때 뒤로가기 버튼 누르면 이전 설정 화면 나오도록 하는 함수!를 재정의함
                                           pref: Preference): Boolean {
        // 새로운 프래그먼트 인스턴스화
        val args = pref.extras
        val fragment = supportFragmentManaer.fragmentFactory.instantiage(
            classLoader,
            pref.fragment)
        fragment.arguments = args
        supportFragmentManager.beginTransaction()
            .replace(R.id.setting_content, fragment)
            .addToBackStack(null)
            .commit()
        return true
    }
}

 

만약 설정 화면이 복잡하다면 메인 설정 화면에서 인텐트를 이용해 하위 설정 화면을 띄우는 방법으로 구현할 수 있다. 이 작업은 코드에서 처리할 수 있지만 설정 XML 등록만으로도 가능하다. 즉, 설정 화면의 항목을 사용자가 클릭했을 때 다른 액티비티를 실행하는 기능을 XML 설정만으로 구현할 수 있다.

// 인텐트로 설정 화면 실행
<Preference
    app:key="activity"
    app:title="Launch activity">
    <intent // 인텐트 이용해 하위 설정 화면 띄우기 위해
        android:targetClass="com.example.test17.SomeActivity"
        // 설정 XML만 등록해도 됨. 사용자가 클릭했을 때 다른 액티비티 실행하도록 함.
        android:targetpackage="com.example.test17" />
</Preerence>

 

<Preference> 태그 하위에 <intent> 태그로 설정 화면을 지정하면 사용자가 이 항목을 클릭했을 때 해당 설정 화면이 실행된다. 이때 다음처럼 엑스트라 정보를 포함할 수도 있다.

// 인텐트에 엑스트라 데이터 포함
<intent // 사용자가 클릭했을 때 다른 액티비티 실행되도록 하는 인텐트
    android:targetClass="com.example.test17.SomeActivity"
    android:targetPackage="com.example.test17">
    <extra // 그 인텐트에 엑스트라 데이터를 넣을 수도 있다.
        android:name="example_key"
        android:value="example_value" />
</intent>

 

<intent> 태그 하위에 <extra> 태그로 인텐트에 포함해서 전달할 엑스트라 데이터를 설정하면 된다. 또한 위 코드처럼 명시적 인텐트 정보뿐만 아니라 암시적 인텐트 정보도 설정할 수 있다.

// 암시적 인텐트 사용
<intent
    android:action="android.intent.action.VIEW"
    android:data="http://www.google.com" />

 

 

설정 제어

이번에는 사용자가 설정 항목을 클릭한 순간의 이벤트를 처리하거나 설정값을 설정 항목 옆에 나타나게 하는 방법을 알아보자. 즉, 코드에서 설정을 제어하는 방법이다.(이전까진 XML 파일에 등록하는 거 살펴봤음)

 

먼저 각 설정 항목에 해당하는 1️⃣객체를 findPreference() 함수로 얻어야 한다. 예를 들어 XML에 다음처럼 <EditTextPreference> 태그를 작성했다고 가정해보자. <EditTextPreference>는 사용자에게 글을 입력받는 설정이다.

// 글을 입력받는 설정
<EditTextPreference // XML에 설정하는 설정 항목 중 사용자에게 글을 입력받는 설정을 위한 태그
    app:key="id"
    app:title="ID 설정"
    app:isPreferenceVisible="false" /> // false로 설정하여 화면에 나오지 않음

<EditTextPreference> 태그를 이용하면 실제로 EditTextPreference 라는 클래스의 객체가 생성되므로 이 객체를 코드에서 얻어 해당 설정 항목을 제어할 수 있다.

예를 들어 XML에서는 <EditTextPreference> 태그의 isPreferenceVisible 속성값을 false로 지정하여 화면에 나오지 않게 하고, 코드에서 EditTextPreference 객체를 얻어 isPreferenceVisible 값을 true로 바꿔보자.

// 설정값을 코드에서 바꾸기
overide fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
    setPreferencesFromResource(R.xml.settings, rootKey)
    val idPreference: EditTextpreference? = findPreference("id")
    // 코드에서 EditPreference 객체를 얻고
                           // findPreference() 함수의 매개변수는 XML에서 작성한 각 설정 태그의 key 속성값
    idPreference?.isVisible = true // 속성값을 true로 바꿔서 화면에 보이게 함
    
    idPreferene?.summary="code summary"
    idPreference?.title="code title"
}

findPreference() 함수의 매개변수는 XML에서 작성한 각 설정 태그의 key 속성값이다. 예로 든 isVisible 값 외에 설정 항목 이름을 나타내는 title이나 항목 이름 옆에 설정값을 나타내는 summary 등도 코드에서 상황에 맞게 조정할 수 있다.

 

그런데 사용자가 입력하는 글을 설정하는 <EditTextPreference>나 제시한 목록에서 선택하는 <ListPreference>는 설정값을 summary 속성에 자동으로 지정해야 할 수도 있다. 이때 SimpleSummaryProvider를 사용한다. 예를 들어 설정 XML을 다음처럼 작성했다고 가정해보자.

// 설정 XML 예
<EditTextPreference
    app:key="id"
    app:title="ID 설정" />

<ListPreference // 목록에서 하나를 선택해서 설정하는 태그
    app:key="color"
    app:title="색상 선택"
    android:entries="@array/my_color" // 사용자에게 보여줄 목록을 지정하는 속성
    app:entryValues="@array/my_color_values" /> // 사용자가 선택한 설정값을 지정하는 속성

<ListPreference>는 목록에서 하나를 선택해서 설정하는 태그이다. <Listpreference>을 이용할 때는 사용자에게 보여 줄 목록을 entries 속성에 지정하고 사용자가 선택한 설정값은 entryValus 속성으로 지정한다. 그런데 위처럼 작성하고 실행하면 설정한 값이 화면에 나오지 않는다.

 

사용자가 입력한 값이나 선택한 값을 summary 에 자동으로 지정하려면 SimpleSumaryProvider를 이용한다. 이 객체를 EditTextPreference와 EditTextPreference와 ListPreference에 적용하면 사용자가 설정하지 않았다면 'Not set'이라는 문자열이 출력되고, 사용자가 설정하면 해당 문자열이 출력된다.

// 설정값 자동 적용
val idPreference: EditTextPreference? = findPreference("id")
val colorPreference: ListPreference?  = findPreference("color")
// 함수로 설정 태그의 key 속성값 전달하면서 각 설정 항목의 객체 얻음

idPreference?.summaryProvider =
    EditTextPreference.SimpleSummaryProvider.getInstance()
    // SimpleSumaryProvider 객체를 각 설정항목의 객체에 적용함
colorPreference?.summaryProvider =
    ListPreference.SimpleSummaryProvider.getIntance()

그리고 SummaryProvider의 하위 클래스를 만들어 코드에서 원하는 대로 summary가 지정되게 할 수도 있다.

// 코드에서 설정값 표시하기
idPreference?.summaryProvider =
    Preference.SummaryProvider<EditTextPreference> { preference ->
    // SummaryProvider의 하위 클래스
        val text = preference.text
        if (TextUtils.isEmpty(text)) { // 코드에서 지정한 summary 내용
            "설정이 되지 않았습니다."
        } else {
            "설정된 ID 값은 : $text 입니다."
        }
    }

설정 항목에 이벤트를 추가할 수도 있으며, 이벤트를 처리해야 하다면 setOnPreferenceClickListener() 함수로 이벤트 핸들러를 지정하면 된다.

// 이벤트 핸들러 지정
idPreference?.setOnPreferenceClickListener { preference ->
    Log.d("kkang", "preference key : ${preference.key}")
    true
}

 

설정한 값 가져오기

프리퍼런스를 이용하면 설정한 내용이 XML 파일로 저장된다. 저장은 자동으로 되지만 설정값을 가져올 때는 PreferenceManager.getDefaultSharedPreferences() 함수를 이용한다.

예를 들어 다음처럼 설정 XML을 작성했다고 가정해보자.

// 설정 XML
<EditTextPreference // 사용자에게 글을 입력받는 설정
    app:key="id" // 사용자가 입력한 내용이 저장되는 키값
    app:title="ID 설정" /> // title 속성은 설정 화면에 출력되는 문자열

그러면 사용자가 입력한 값은 id키값으로 저장된다. 이 값을 가져올 때는 다음처럼 작성한다.

// 설정값 가져오기
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
val id = sharedPreferences.getString("id", "") // 저장된 설정값을 키값으로 불러오기

 

설정 변경 순간 감지

때로는 사용자가 설정을 변경하는 순간을 감지변경한 값을 이용해야 할 수도 있다. 이처럼 설정이 변경되는 것을 감지하는 방법은 2가지인데 하나는 1️⃣Preference.OnPreferenceChangeListener를 이용하는 방법이고, 다른 하나는 2️⃣SharedPreferences.OnSharedPreferenceChangeListener를 이용하는 방법이다. 전자는 프리퍼런스 객체마다 이벤트 핸들러를 직접 지정하여 객체의 설정 내용이 변경되는 순간의 이벤트를 처리한다. 후자는 모든 설정 객체의 변경을 하나의 이벤트 핸들러에서 처리한다.

다음은 Preference.OnPreferenceChangeListener(각 프리퍼런스마다 이벤트 핸들러 지정)를 이용해 이벤트 콜백 함수의 매개변수로 이벤트가 발생한 Preference 객체와 바뀐 값을 가져오는 코드이다.

// 프리퍼런스를 이용한 이벤트 처리
idPreference?.setOnPreferenceChangeListener { preference, newValue ->
// 이벤트 콜백 함수의 매개변수로 이벤트가 발생한 Preference 객체와 바뀐 값(newValue)를 가져옴
    Log.d("kkang", "preference key : ${preference.key}, newValue : ${newValue}")
    true
}

만약 SharedPreferences.OnsharedPreferenceChangeListener(모든 설정 객체의 변경을 하나의 이벤트 핸들러로 처리)를 이용한다면 1️⃣설정 프래그먼트 클래스에서 SharedPrefeences.OnSharedPreferenceChangeListener를 구현하고 추상함수인 onSharedPreferenceChanged() 함수를 이용헤 이벤트 핸들러를 등록해야 한다. 이벤트 감지가 더 이상 필요하지 않을 때는 unregisterOnSharedPreferenceChangeListener() 함수를 이용해 3️⃣이벤트 등록을 해제한다.

// 공유된 프리퍼런스를 이용한 이벤트 처리
class MySettingFragment : PreferenceFragmentCompat(), // 설정 프래그먼트 클래스에서
SharedPreferences.OnSharedPreferenceChangeListener {
// ChangeListener를 구현
    (... 생략 ...)
    override fun onSharedPreferencechanged(sharredPreferences: // 추상함수를 재정의
                                           SharedPreferences?, key: String?) {
        if (key == "id") {
            Logi("kkang", "newValue : " + sharedPreferences?.getString("id", ""))
        }
    }
    override fun onResume() {
        super.onResume()
        preferenceManager.sharedPreferences
            .registerOnSharedPreferenceChangeListener(this)
            // 이벤트 핸들러 등록
    }
    override fun onPause() {
        super.onPause()
        preferenceMnager.sharedPreferences
            .unregisterOnSharedPreferenceChangeListener(this)
            // 이벤트 등록 해제
    }
}

 

'깡샘 코틀린' 카테고리의 다른 글

18-2 HTTP 통신하기  (0) 2023.07.13
18-1 스마트폰 정보 구하기  (0) 2023.07.12
17-2 파일에 보관하기  (0) 2023.07.12
17-1 저장소에 데이터 보관하기  (0) 2023.07.09
16-2 안드로이드 기본 앱과 연동하기  (0) 2023.07.09