앱에서 환경설정을 위해 액티비티에서 설정 화면을 구성하고, 그 화면에서 발생하는 사용자 이벤트를 처리하여 설정한 데이터를 영속적으로 저장해야 합니다.
이러한 부분을 대행해주는 것이 Preference 라이브러리입니다.
공식 사이트에서 환경설정은 Preference 라이브러리를 사용하는 것을 추천하고 있습니다.
module 수준의 build.gradle 파일에 다음과 같이 preference 라이브러리를 추가합니다.
dependencies {
// 작성일 시점 최신버전
implementation 'androidx.preference:preference-ktx:1.1.1'
}
환경설정을 위한 XML 파일을 준비해야 합니다. 이 파일은 res 하위에 xml 폴더 안에 만듭니다.
루트 태그는 PreferenceScreen으로 설정하고, 그 안에 여러 종류의 Preference를 추가하면 됩니다.
Preference 태그는 다음과 같습니다.
자주 사용하는 태그와 그 안의 속성들에 대해 알아보겠습니다.
CheckBoxPreference와 SwitchPreference는 true, false를 위한 값을 설정하는데 사용하는 Preference입니다.
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<CheckBoxPreference
android:key="vibrate"
android:summary="진동 울림으로 알림을 받으려면 체크하세요"
android:title="진동" />
<SwitchPreference
android:key="message"
android:summary="메시지 알림을 받으려면 체크하세요"
android:title="메시지 알림" />
</PreferenceScreen>
세 개의 속성의 정의는 다음과 같습니다.
key : Preference를 사용하면 SharedPreferences를 이용하여 설정 내용을 자동으로 저장합니다. 이 때, 저장은 key-value 형식으로 하는데 여기서 key에 해당하는 속성입니다.
title : 화면에 출력해주는 문자열
summary : 화면에 출력해주는 문자열
사용자에게 간단한 글을 입력받아 설정하는 태그입니다.
<EditTextPreference
android:dialogTitle="닉네임 설정"
android:key="nickname"
android:summary="닉네임을 설정하세요"
android:title="닉네임" />
사용자에게 목록을 띄우고 항목을 선택하는 태그입니다. 이 Preference를 선택하면 다이얼로그가 열리고, ListPreference는 항목 하나를 선택하는 것이고 MultiSelectListPreference는 항목을 여러 개 선택하는 것입니다.
<SwitchPreference
android:key="sound"
android:title="소리" />
<ListPreference
android:dependency="sound"
android:entries="@array/array_voice"
android:entryValues="@array/array_voice"
android:key="sound_list"
android:summary="카톡"
android:title="알림음" />
<MultiSelectListPreference
android:entries="@array/array_voice"
android:entryValues="@array/array_voice"
android:key="sound_list2"
android:summary="카톡"
android:title="알림음" />
entries : 사용자에게 보여지는 다이얼로그의 항목 문자열입니다.
entryValues : 선택한 항목을 위해 저장되는 값입니다. SharedPreferences에 key는 key 태그로, value는 이 값으로 저장됩니다.
dependency : Preference의 결합 관계를 설정하는 속성입니다. 예를 들어, 위에서는 ListPreference에 dependency 속성으로 SwitchPreference를 설정하였습니다. 이렇게 되면 SwitchPreference가 true일 때만 ListPreference가 활성화되고, 클릭하여 항목을 선택할 수 있습니다.
관계가 있는 Preference 여러 개를 묶어서 표현하기 위한 일종의 서브 타이틀 정도의 개념입니다.
<PreferenceCategory android:title="알림설정">
<CheckBoxPreference
android:key="vibrate"
android:summary="진동 울림으로 알림을 받으려면 체크하세요"
android:title="진동" />
<SwitchPreference
android:key="message"
android:summary="메시지 알림을 받으려면 체크하세요"
android:title="메시지 알림" />
<EditTextPreference
android:dialogTitle="닉네임 설정"
android:key="nickname"
android:summary="닉네임을 설정하세요"
android:title="닉네임" />
</PreferenceCategory>
관계 있는 Preference들을 묶는 방법은 PreferenceCategory도 있지만, Preference를 사용하면 새로운 창으로 화면을 변경할 수 있습니다. 이는 아래에서 추가로 설명하겠습니다.
<Preference
android:key="keyword_screen"
android:summary="사용 안 함"
android:title="키워드 알림"
app:fragment="kr.co.lee.preferenceexapmle.NestedSettingPreferenceFragment" />
위에서 만든 XML 파일을 적용시키기 위해서 PreferenceFragmentCompat 클래스를 상속받는 클래스를 선언합니다.
이 클래스에서 XML 파일을 inflate하고 그 안의 Preference들을 초기화하여 사용합니다.
// PreferenceFragmentCompat을 상속받는 클래스 정의
class NestedSettingPreferenceFragment: PreferenceFragmentCompat() {
lateinit var prefs: SharedPreferences
var keywordPreference: Preference? = null
var keywordListPreference: Preference? = null
// onCreate() 중에 호출되어 Fragment에 preference를 제공하는 메서드
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// preference xml을 inflate하는 메서드
setPreferencesFromResource(R.xml.nested_setting_preference, rootKey)
// rootkey가 null이라면
if (rootKey == null) {
// Preference 객체들 초기화
keywordPreference = findPreference("keyword")
keywordListPreference = findPreference("keyword_sound_list")
}
}
}
XML을 inflate하기 위해서 setPreferencesFromResource(int, String) 메서드를 사용합니다.
XML을 inflate 하지 않고, 코드에서 계층구조를 만들려면 PreferenceManager.createPreferenceScreen(Context)로 루트 태그(PreferenceScreen)를 만들고 PreferenceGroup.addPreference(Preference)로 루트태그에 원하는 preference를 추가합니다. 그 후 setPreferenceScreen(PreferenceScreen)를 사용하여 적용하면 됩니다.
Preference는 SharedPreferences를 사용하여 데이터를 저장합니다.
데이터는 자동으로 저장이 되는데, 이 데이터를 사용하기 위해서는 SharedPreferences 객체를 얻어야 합니다. 객체는 PreferenceManager.getDefaultSharedPreferences() 함수를 사용하여 얻습니다.
이 함수는 앱의 어디서든 사용할 수 있고, xml에서 정의한 key 속성의 값을 사용하여 value를 얻어오는 형식입니다.
xml 파일
<EditTextPreference
app:key="signature"
app:title="Your signature"/>
PreferenceFragmentCompat을 상속받는 Fragment 클래스
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this /* Activity context */)
val name = sharedPreferences.getString("signature", "")
위의 예시는 xml에 EditTextPreference의 key로 "signature"라고 설정하고 그 xml을 inflate한 클래스안에서 SharedPreferences 객체를 얻은 후 키로 "signature"를 주어 EditTextPreference안에 저장된 값을 얻어오는 예시입니다.
Preference의 데이터는 기본적으로 SharedPreferences를 사용하여 저장합니다.
하지만 SharedPreferences는 항상 좋은 방법이 아닙니다.
예를 들어, 애플리케이션에서 사용자가 로그인해야하는 경우 설정이 다른 디바이스 및 플랫폼이 반영되도록 클라우드에서 환경설정을 유지할 수 있습니다. 아니면 프로그램에 장치별 옵션이 따로 있는 경우 장치의 각 사용자는 별도의 설정을 갖게 됩니다.
이런 경우에는 PreferenceDataStore를 사용하면 커스텀 된 storage backend에 preference 데이터를 저장할 수 있습니다.
사용자가 환경을 설정하는 순간 이벤트 처리가 필요할 수 있습니다. 이벤트를 처리하지 않아도 설정 내용이 자동으로 저장되지만, 설정 내용에 따라 서버와 연동되거나 summary를 변경하는 등의 작업이 수행되는 경우입니다.
이를 위해서 사용하는 리스너 인터페이스는 아래와 같습니다(이벤트 모델 형식)
Preference.OnPreferenceChangeListener : 각각의 Preference에 적용됩니다.
SharedPreferences.OnSharedPreferenceChangeListener : 모든 Preferences에 적용됩니다.
여기서는 SharedPreferences.OnSharedPreferenceChangeListener를 사용하는 방법을 알아보겠습니다.
// 설정 변경 이벤트 처리
val prefListener =
SharedPreferences.OnSharedPreferenceChangeListener {
sharedPreferences: SharedPreferences?, key: String? ->
// key는 xml에 등록된 key에 해당
when (key) {
"keyword_sound_list" -> {
// SharedPreferences에 저장된 값을 가져와서 summary 설정
val summary = prefs.getString("keyword_sound_list", "카톡")
keywordListPreference?.summary = summary
}
"keyword" -> {
val value = prefs.getBoolean("keyword", false)
}
}
}
// 리스너 등록
override fun onResume() {
super.onResume()
prefs.registerOnSharedPreferenceChangeListener(prefListener)
}
// 리스너 해제
override fun onPause() {
super.onPause()
prefs.unregisterOnSharedPreferenceChangeListener(prefListener)
}
OnSharedPreferenceChangeListener를 구현하는 객체를 만드는데, 매개변수로 key가 넘어옵니다. 이 key는 xml에 등록된 preference의 key로 이를 분기하여 이벤트가 발생한 preference 객체를 구분합니다. 그 후 해당하는 preference에 summary를 변경하는 등의 코드를 작성하면 됩니다,
그리고 적절한 생명주기를 관리하기 위하여 리스너의 등록과 해제를 생명주기 함수에 작성합니다.
PreferenceFragment를 사용하기 위해서 Activity나 Fragment의 xml에 FragmentContainerView를 추가합니다.
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/show_setting" />
그 후, 코드에서 FragmentManager와 FragmentTransaction을 사용하여 FragmentContainerView에 Fragment를 출력합니다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// FragmentManager를 얻어온 후 transaction 시작
// contatiner에 Fragment 변경하고 commit을 호출하여 transaction 적용
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, SettingPreferenceFragment(), "setting_fragment")
.commit()
}
위에서 설명했듯이, 관계 있는 Preference를 연결하기 위해서는 PreferenceCategory를 사용할 수 있지만 Preference를 이용할 수도 있습니다. Preference를 이용하면 새로운 화면로 변경하여 관계 있는 설정들을 보여주는 것입니다.
이를 위해서 새로운 화면의 Preference xml을 작성하고, PreferenceFragmentCompat을 상속 받는 클래스를 선언한 후 그 xml을 지정합니다. 그리고 새로운 창을 띄울 xml의 Preference 태그 안에 app:fragment 속성으로 클래스의 패키지명이 포함된 전체 경로를 주면 됩니다.
<Preference
android:key="keyword_screen"
android:summary="사용 안 함"
android:title="키워드 알림"
app:fragment="kr.co.lee.preferenceexapmle.NestedSettingPreferenceFragment" />
마지막으로 PreferenceFragment를 호스팅하는 Activity 코드안에 아래와 같은 코드를 추가합니다.
class MyActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
...
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference)
: Boolean {
// Instantiate the new Fragment
val args = pref.extras
val fragment = supportFragmentManager.fragmentFactory.instantiate(
classLoader,
pref.fragment)
fragment.arguments = args
fragment.setTargetFragment(caller, 0)
// Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction()
.replace(R.id.settings_container, fragment)
.addToBackStack(null)
.commit()
return true
}
}
Preference 테그를 누를 경우 특정한 action을 가지도록 만들 수 있습니다. 이를 위해 Preference 태그에 Intent를 달거나, OnPreferenceClickListenrer를 설정하면 됩니다.
Preference에서 새로운 Fragment나 Activity를 실행하기 위해 Intent를 설정할 수 있습니다. 이 방법은 Context.startActivity()를 실행하는 것과 같은 방법입니다.
<Preference
app:key=”activity”
app:title="Launch activity">
<intent
android:targetPackage="com.example"
android:targetClass="com.example.ExampleActivity">
<extra
android:name="example_key"
android:value="example_value"/>
</intent>
</Preference>
val intent = Intent(context, ExampleActivity::class.java)
intent.putExtra("example_key", "example_value")
activityPreference.setIntent(intent)
로직이 복잡하다면 OnPreferenceClickListener를 사용할 수 있습니다. 해당 리스너는 onPreferenceClick() 콜백을 호출합니다.
onClickPreference.setOnPreferenceClickListener({
// do something
true
})
간단하게 소스코드를 작성하여 예제를 만들었습니다.
이 글은 깡썜의 안드로이드 프로그래밍 및 안드로이드 공식 사이트를 보고 작성하였습니다.
안드로이드 developer - settings
틀린 부분을 댓글로 남겨주시면 수정하겠습니다..!!