์ด ๊ธ์ Hello DataStore, Bye SharedPreferences๐ โ Android๐ฑ โ Part 1: Preference DataStore์ ๋ฒ์ญํ ํฌ์คํธ.
Jetpack DataStore
๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ธฐ ์ํ ์๋จ์ด๋ค.
์ด๋ฅผ ํตํด SharedPreferences
์ฒ๋ผ Key-Value Pair๋ Protocol Buffer๋ฅผ ์ด์ฉํ Typed Objects๋ฅผ ์ ์ฅํ ์ ์๋ค. (๋ค์ ํฌ์คํธ์์ ๋ค๋ฃฐ ์์ )
DataStore
๋ Kotlin
, Coroutine
๋ฐ Flow
๋ฅผ ์ฌ์ฉํ์ฌ ์ผ๊ด์ฑ๊ณผ ํธ๋์ญ์
์ ์ง์ํจ์ผ๋ก์ ๋ฐ์ดํฐ๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ์ ์ฅํ๋ค.
๊ฐ๋จํ ๋งํด์ DataStore
๋ SharedPreferences
๋ฅผ ๋์ฒดํ๋ ์๋ก์ด ๋ฐ์ดํฐ ์ ์ฅ์๋จ์ด๋ค.
๊ฐ์ฅ ์ข์ํ๋ ์ด์ ๋ก๋ Kotlin
, Coroutine
๋ฐ Flow
๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ด๋ค.
SharedPreferences
๋ ๋๊ธฐ API๋ฅผ ์ ๊ณตํ๋ ๊ฒ๊ณผ MAIN ์ค๋ ๋๋ก๋ถํฐ ์์ ํ์ง ์์ ๋จ์ ์ด ์๋ค ๋ฐ๋ฉด DataStore
๋ ๋ด๋ถ์ ์ผ๋ก Dispatchers.IO
๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ UI ์ค๋ ๋์์ ์ฌ์ฉํ๊ธฐ์ ์์ ํ๋ค.
๋ฐํ์ ์๋ฌ๋ก๋ถํฐ ์์ ํ๋ค.
SharedPreference
์์ DataStore
๋ก ๋ง์ด๊ทธ๋ ์ด์
ํ ์ ์๋ ๋ฐฉ๋ฒ๋ ์ ๊ณตํด์ค๋ค.
Protocol Buffer
๋ฅผ ์ฌ์ฉํ์ฌ Type safety ํ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋ค
๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ธฐ ์ํด ๋ ๊ฐ์ง ํ์ ๊ตฌํ์ ์ ๊ณตํ๋๋ฐ
Preference DataStore : ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ธฐ ์ํด Key-Value Pair์ ์ฌ์ฉํ๋ค. ๊ทธ๋ฌ๋ ์ด๊ฑด Type safety ํ์ง ๋ชปํจ
Proto DataStore : ๋ฐ์ดํฐ๋ฅผ Protocol Buffer๋ฅผ ์ฌ์ฉํ์ฌ ์ปค์คํ ํ์ ์ ํํ๋ก ์ ์ฅํ๋ค (๋ค์ ํฌ์คํธ์์ ๋ค๋ฃฐ ์์ )
DataStore
์ ๋ํ ์๊ฐ๋ ์ด์ ๋๋ฉด ์ถฉ๋ถํ ๊ฒ ๊ฐ๊ณ ์ด์ ์ฝ๋๋ฅผ ์์ฑํด๋ณด์
DataStore์ ๊ดํ ์์ ๋ ์ด Repository์์ ํด๋ก ํ๊ฑฐ๋ ์ฐธ์กฐํ ์ ์๋ค.
์ฌ์ฉ์์ UI ๋ชจ๋๋ฅผ (Ex : ๐ Light Mode ๋๋ ๐ Dark Mode) ์ ์ฅํ๋ ์ํ Android ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ๋ฐํ ๊ฒ์ด๋ค.
dependencies {
// Preferences DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha08"
}
๋จผ์ ์ฑ ๋ชจ๋์ build.gradle
์ Gradle ์ข
์์ฑ์ ์ถ๊ฐํ๋ค.
First of all, letโs add a Gradle dependency in build.gradle of your app module. Currently 1.0.0-alpha01 is the latest release. You can keep an eye here to get info about the latest version.
์๊ธ์ด ์์ฑ๋ ์์ ์๋ 1.0.0-alpha01
์ด ์ต์ ๋ฒ์ ์ด์์ง๋ง ํ์ฌ๋ 1.0.0-alpha08
์ด ์ต์ ๋ฒ์ ์ด๋ค. ๊ฐ์ฅ ์ต์ ๋ฒ์ ์ ์ฌ๊ธฐ์ ํ์ธํ ์ ์๋ค
Dark๋ Light๊ฐ์ UI ๋ชจ๋๋ฅผ ์ํ enum class
๋ฅผ ์๋์ฒ๋ผ ์์ฑํด์ค๋ค
enum class UiMode {
LIGHT, DARK
}
๊ทธ ๋ค์์ SettingsManager
๋ผ๋ ํด๋์ค๋ฅผ ๋ง๋ค์ด์ค๊ฑด๋ฐ ์ด ํด๋์ค๋ ์ฑ์์ ์ฌ์ฉ์๊ฐ ์ค์ ํ ๊ฐ์ ๊ด๋ฆฌํด์ฃผ๋ ์ญํ
class SettingsManager(context: Context) {
private val dataStore = context.createDataStore(name = "settings_pref")
...
์ด ํด๋์ค๋ dataStore
ํ๋๋ฅผ DataStore
๋ฅผ ์ด์ฉํด settings_pref
๋ผ๋ ์ด๋ฆ์ผ๋ก ์ด๊ธฐํ๋๋ค.
createDataStore()
๋ context
์ ์ต์คํ
์
๋ฉ์๋
์ด์ UI ๋ชจ๋๋ฅผ ํค๋ฅผ ์ด์ฉํ์ฌ (SharedPreference
์ฒ๋ผ) ์ค์ ํ ๊ฑด๋ฐ ํค๋ ์๋์ฒ๋ผ ์์ฑ๋์ด์๋ค.
์๋ ์ฝ๋๋ฅผ SettingsManager
ํด๋์ค ๋ด๋ถ์ ์์ฑํด์ผํจ
companion object {
val IS_DARK_MODE = preferencesKey<Boolean>("dark_mode")
}
๐ ์ด๊ฒ์ด ์์ฑ๋ ํค์ธ IS_DARK_MODE
๊ณ ์ด๊ฑด boolean
๊ฐ์ผ๋ก ์ ์ฅ๋๋๋ฐ false
๋ฉด Light mode, true
๋ฉด Dark mode. ์ด๋ฐ ์์ผ๋ก ์ฌ์ฉํ๋ค.
์ด๋ ๊ฒ ์ฌ์ฉํ๋ ์ด์ ๋ DataStore
๋ ์คํค๋ง๋ฅผ ๋ฏธ๋ฆฌ ์ ์ํ์ง ์์๊ธฐ ๋๋ฌธ์ด๊ณ
DataStore<Preferences>
์ ์ ์ฅํด์ผํ๋ ๊ฐ ๊ฐ์ ๋ํ ํค๋ฅผ ์ ์ํ๋ ค๋ฉด ๋ฐ๋์ Preferences.preferencesKey()
๋ฅผ ์ฌ์ฉํด์ผํ๋ค.
์ด์ UI/Acitivty์์ UI ๋ชจ๋๋ฅผ ์ค์ ํ๋ ๋ฉ์๋๋ฅผ ๋ง๋ค์ฐจ๋ก์ธ๋ฐ
์ฐธ๊ณ : Preferences DataStore๋
DataStore
์ ๊ฐ์ transactionalํ๊ฒ ์ ๋ฐ์ดํธํ๋edit()
๋ฉ์๋๋ฅผ ์ ๊ณตํ๋ค
suspend fun setUiMode(uiMode: UiMode) {
dataStore.edit { preferences ->
preferences[IS_DARK_MODE] = when (uiMode) {
UiMode.LIGHT -> false
UiMode.DARK -> true
}
}
}
์ด์ ์ค์ ์ ๊ฐ์ ธ์์ผํ๋๋ฐ DataStore๋ Flow
๋ฅผ ์ด์ฉํด์ ์ค์ ๊ฐ์ ๋๋ฌ๋ด์ฃผ๋ data
๋ผ๋ ํ๋กํผํฐ๋ฅผ ์ ๊ณตํ๋ค.
Flow
๋ฅผ ํ์ฉํ ๋๋ค. ์๋ ์ฝ๋๋ฅผ ์ฐธ์กฐ ๐
val uiModeFlow: Flow<UiMode> = dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}
.map { preference ->
val isDarkMode = preference[IS_DARK_MODE] ?: false
when (isDarkMode) {
true -> UiMode.DARK
false -> UiMode.LIGHT
}
}
๐ Flow
๋ฅผ ์ฌ์ฉํ ๊ฒ์ ๋ณผ ์ ์๋ค. uiModeFlow
ํ๋๋ ์ค์ ์ด ํธ์ง/์
๋ฐ์ดํธ ๋ ๋๋ง๋ค ๊ฐ์ ๋ด ๋ณด๋. ์ฐ๋ฆฌ๋ ๋ฐ์ดํฐ ์ ์ฅ์์ boolean
์ ์ ์ฅํ๋ค.
map {}
์ ์ฌ์ฉํ์ฌ boolean
๊ฐ์ Ui ๋ชจ๋, ์ฆ UiMode.LIGHT
๋๋ UiMode.DARK
์ ๋งคํํ๋ค.
์ฐธ๊ณ : DataStore๋ ๊ฐ ์ฝ๊ธฐ์ ์คํจํ๋ฉด IOException์ ๋ฐ์์ํด. ๊ทธ๋์ ์ฐ๋ฆฌ๋
emptyPreferences ()
๋ฅผ ๋ฐฉ์ถํจ์ผ๋ก์จ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ค.
์ด๊ฒ์ด DataStore
์ค์ ์ ๊ดํ ๋ชจ๋ ๊ฒ์ด๋ค ๐, ์ด์ UI๋ฅผ ๋์์ธ ํด๋ณด๋ฉด
์กํฐ๋นํฐ์๋ UI ๋ชจ๋๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ ์ด๋ฏธ์ง ๋ฆฌ์์ค, ์ฆ ๐ ๋ฐ ๐์ด์๋ ImageButton
๋ง ์๊ณ
class MainActivity : AppCompatActivity() {
private lateinit var settingsManager: SettingsManager
private var isDarkMode = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
settingsManager = SettingsManager(applicationContext)
observeUiPreferences()
initViews()
}
initViews()
์์๋ ImageButton
์ ํด๋ฆญํ ๋ UI ๋ชจ๋๋ฅผ ๋ณ๊ฒฝํด์ฃผ๋ ์ฒ๋ฆฌ๋ฅผ ์ค์ ํด์ค๋ค.
private fun initViews() {
imageButton.setOnClickListener {
lifecycleScope.launch {
when (isDarkMode) {
true -> settingsManager.setUiMode(UiMode.LIGHT)
false -> settingsManager.setUiMode(UiMode.DARK)
}
}
}
}
observeUiPreferences()
์์๋ ์ค์ ์ด ์
๋ฐ์ดํธ ๋ ๋๋ง๋ค ๊ฐ์ ๋ด๋ณด๋ด๋ Flow๐ ์ธ SettingsManager์ ์กด์ฌํ๋ uiModeFlow
ํ๋๋ฅผ ์ฌ์ฉํ์ฌ UI ๋ชจ๋ ๊ธฐ๋ณธ ์ค์ ์ observeํ๋ค.
private fun observeUiPreferences() {
settingsManager.uiModeFlow.asLiveData().observe(this) { uiMode ->
when (uiMode) {
UiMode.LIGHT -> onLightMode()
UiMode.DARK -> onDarkMode()
}
}
}
๐ LiveData
์ Flow
์์ ๋ฐฉ์ถ ๋ ๊ฐ์ ์ ๊ณตํ๋ flow ์ต์คํ
์
์ธ asLiveData()
๊ฐ ์๋ค
UI ๋ชจ๋๊ฐ ๋ณ๊ฒฝ๋๋ฉด ์ด๋ฏธ์ง ๋ฆฌ์์ค์ ๋ฃจํธ ๋ ์ด์์์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ปฌ๋ฌ๋ง ์
๋ฐ์ดํธํจ. (์ค์ Dark/Light ๋ชจ๋๋ AppCompatDelegate.setDefaultNightMode()
๋ฅผ ์ฌ์ฉํ์ฌ ๋ณ๊ฒฝํ ์ ์๋ค.)
์ด์ ์ฑ์ ์คํํ ์๊ฐ์ด๋ค ๐. ์ด ์ฑ์ ์คํํ๋ฉด ์๋๊ฐ์ ํ๋ฉด์ ๋ณผ ์ ์๋๋ฐ
๋ฉ์ง๋ค! ๐, ๊ทธ๋ ์ง ์๋์?
์ด๊ฒ์ด SharedPreferences
๋์ Preferences DataStore
๋ฅผ ๊ตฌํ ํ ๋ฐฉ๋ฒ์ด๋ค.
DataStore
๋ ํ์ฌ ์ํ ๋ฒ์ ์ด๋ฏ๋ก ์์ผ๋ก ๋ ๋ง์ ๋ฒ์ ์ด ์ถ์ ๋ ์์ ์ด๋ค ๐ฃ๏ธ.
DataStore
๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ธฐ ์ํด ํ์ผ ๊ด๋ฆฌ์์ ๋งค์ปค๋์ฆ์ ์ฌ์ฉํ๊ณ ์๋ค. ๊ทธ๋ฌ๋ ์ด๊ฒ์ SharedPreferences
์์ ๊ด๋ฆฌ๋๋ ๊ฒ ๊ณผ๋ ๋ค๋ฅด๋ค.
๋ฐ์ดํฐ๊ฐ ์ด๋ป๊ฒ ์ ์ฅ๋๋์ง ์๊ณ ์ถ์ผ๋ฉด ์๋๋ก์ด๋ ์คํ๋์ค์ Device File Explorer
๋ฅผ ์ด์ฉํ๋ฉด ๋๋๋ฐ /data/app/YOUR_APP_PACKAGE_NAME/files/datastore
์ด ๊ฒฝ๋ก๋ก ์ด๋ํ ์ ์๊ณ ์๋์ ํ์ผ์ ๋ณผ ์ ์๋ค.
๊ทธ๋ฌ๋ settings_perf.preferences_pb
ํ์ผ์ ์๋์ฒ๋ผ ์ฝ์ ์ ์๋ค