
์ด ๊ธ์ 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 ํ์ผ์ ์๋์ฒ๋ผ ์ฝ์ ์ ์๋ค
