안드로이드 Preferences DataStore 사용법과 예제
MVVM 패턴을 공부하면서 함께 적용할 Jetpack Architecture에 대해 공부하고 있다.
왠만한건 한 번씩 다 봤지만, DataStore란 녀석은 본 적이 없어서 이번 기회에 제대로 공부하기로 했다.
앱이 종료되어도 저장되어야할 작은 데이터는 주로 SharedPreferences를 사용해왔다.
하지만 이제는 구글에서 DataStore로 이전하기를 권장하고있다.
SharedPreferences의 한계는 다음과 같다.
1. UI스레드에서 호출할 수 있도록 API가 설계되었지만, UI스레드를 블로킹해 ANR을 발생시킬 수 있다.
2. 다중스레드 환경에서 안전하지 않다.
3. type safety가 보장되지 않는다.
이러한 문제를 해결하기위해 DataStore가 만들어졌다.
그럼, 예제를 함께 살펴보자.
1. dependency 추가
dependencies {
//prefer datastore
implementation "androidx.datastore:datastore-preferences:1.0.0"
}
app수준의 build.gradle에 위와같이 종속성 추가를 해준다.
2. Preferences DataStore 만들기
UserManager.kt 파일을 만들고 전역 프로퍼티로 아래의 코드를 추가해준다.
val Context.dataStore : DataStore<Preferences> by preferencesDataStore(name = "user_prefs")
dataStore를 싱글톤으로 만들어서 DataStore<Preferences> 객체를 만듭니다.
name 매개변수는 Preferences DataStore의 이름입니다.
3. Preferences DataStore에서 읽고 쓰기
class UserManager(
private val dataStore: DataStore<Preferences>
) {
companion object { //1
val USER_AGE_KEY = intPreferencesKey("USER_AGE")
val USER_FIRST_NAME_KEY = stringPreferencesKey("USER_FIRST_NAME")
val USER_LAST_NAME_KEY = stringPreferencesKey("USER_LAST_NAME")
val USER_GENDER_KEY = booleanPreferencesKey("USER_GENDER")
}
suspend fun storeUSer(
age: Int,
frontName: String,
lastName: String,
isMale: Boolean,
) {
dataStore.edit { //2
it[USER_AGE_KEY] = age
it[USER_FIRST_NAME_KEY] = frontName
it[USER_LAST_NAME_KEY] = lastName
it[USER_GENDER_KEY] = isMale
}
}
//3
val userAgeFlow: kotlinx.coroutines.flow.Flow<Int?> = dataStore.data.map {
it[USER_AGE_KEY]
}
val userFirstNameFlow: kotlinx.coroutines.flow.Flow<String?> = dataStore.data.map {
it[USER_FIRST_NAME_KEY]
}
val userLastNameFlow: kotlinx.coroutines.flow.Flow<String?> = dataStore.data.map {
it[USER_LAST_NAME_KEY]
}
val userGenderFlow: kotlinx.coroutines.flow.Flow<Boolean?> = dataStore.data.map {
it[USER_GENDER_KEY]
}
}
1=> 키값 모음
예를 들어, int값의 키를 정의하려면 intPreferencesKey()를 사용한다.
2=> Preferences DataStore에 값 쓰기
SharedPreferences와 비슷하게 edit() 함수를 사용하여 값을 쓴다.
참고로 DataStore는 알아서 코루틴을 사용하여 값을 저장시킨다.
따라서 suspend 함수로 선언한다.
3=> Preferences DataStore에서 값 읽기
DataStore는 값을 저장하거나 읽어올 때 코루틴을 사용하여 Flow로 결과를 전달한다.
DataStore.data 속성을 사용하여 Flow를 받고 map을 이용하여 값을 읽어왔다.
4. activity_main.xml 구성
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:padding="18dp"
android:text="Save user"
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/switch_gender"
app:layout_constraintStart_toStartOf="@+id/switch_gender"
app:layout_constraintTop_toBottomOf="@+id/switch_gender" />
<EditText
android:id="@+id/et_lname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:ems="10"
android:hint="이름"
app:layout_constraintEnd_toEndOf="@+id/et_fname"
app:layout_constraintStart_toStartOf="@+id/et_fname"
app:layout_constraintTop_toBottomOf="@+id/et_fname" />
<EditText
android:id="@+id/et_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:ems="10"
android:hint="나이"
android:inputType="number"
app:layout_constraintEnd_toEndOf="@+id/et_lname"
app:layout_constraintStart_toStartOf="@+id/et_lname"
app:layout_constraintTop_toBottomOf="@+id/et_lname"
tools:layout_editor_absoluteY="317dp" />
<EditText
android:id="@+id/et_fname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:ems="10"
android:hint="나머지 이름"
app:layout_constraintEnd_toEndOf="@+id/tv_gender"
app:layout_constraintStart_toStartOf="@+id/tv_gender"
app:layout_constraintTop_toBottomOf="@+id/tv_gender"
tools:layout_editor_absoluteY="178dp" />
<TextView
android:id="@+id/tv_fname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textColor="@android:color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="firstName"/>
<TextView
android:id="@+id/tv_lname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textColor="@android:color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/tv_fname"
app:layout_constraintStart_toStartOf="@+id/tv_fname"
app:layout_constraintTop_toBottomOf="@+id/tv_fname"
tools:text="lastName"/>
<TextView
android:id="@+id/tv_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textColor="@android:color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/tv_lname"
app:layout_constraintStart_toStartOf="@+id/tv_lname"
app:layout_constraintTop_toBottomOf="@+id/tv_lname"
tools:text="age"/>
<TextView
android:id="@+id/tv_gender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textColor="@android:color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/tv_age"
app:layout_constraintStart_toStartOf="@+id/tv_age"
app:layout_constraintTop_toBottomOf="@+id/tv_age" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_gender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="성별"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="@+id/et_age"
app:layout_constraintStart_toStartOf="@+id/et_age"
app:layout_constraintTop_toBottomOf="@+id/et_age" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
데이터바인딩을 사용했다.(세팅 방법은 생략한다.)
간단하게 설명하면, editText와 switch에 firstName, lastName, 나이, 성별을 입력하고 저장버튼을 누르면 DataStore에 저장한다.
그리고 TextView에는 저장된 데이터를 바로 보여준다.
5. MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var userManager: UserManager
private var age = -1
private var frontName = ""
private var lastName = ""
private var gender = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
userManager = UserManager(dataStore)
binding.run {
buttonSave()
observeData()
}
}
private fun ActivityMainBinding.buttonSave() {
btnSave.setOnClickListener {
frontName = etFname.text.toString()
lastName = etLname.text.toString()
age = etAge.text.toString().toInt()
val isMale = switchGender.isChecked
CoroutineScope(Dispatchers.IO).launch {
userManager.storeUSer(age, frontName, lastName, isMale)
}
}
}
private fun observeData() {
userManager.userAgeFlow.asLiveData().observe(this) {
if(it != null) {
age = it
binding.tvAge.text = it.toString()
}
}
userManager.userFirstNameFlow.asLiveData().observe(this) {
if (it != null) {
frontName = it
binding.tvFname.text = it
}
}
userManager.userLastNameFlow.asLiveData().observe(this) {
if (it != null) {
lastName = it
binding.tvLname.text = it
}
}
userManager.userGenderFlow.asLiveData().observe(this) {
if (it != null) {
gender = if (it) "남성" else "여성"
binding.tvGender.text = gender
}
}
}
}
observeData() 함수에 보면 Flow를 LiveData로 변환시켜서 관찰하는데
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
위 종속성을 추가해주어야 asLiveData() 메소드를 사용가능하다.