iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Mobile Development

現代Android jetpack compose開發入門系列 第 24

Day 24:紀錄零碎信息,使用DataStore

  • 分享至 

  • xImage
  •  

上一章節介紹了怎麼長久的保留規整資料,但是我們除了這些todo之外,還要記錄使用者的習慣或設定等訊息,這時候就需要DataStore了

介紹DataStore

DataStore有兩種,一個是Preference datastore,另一個是Proto datastore,本篇會聚焦在Preference上,但也會稍微解釋不同之處與原因

Preference

這是本篇會使用的設定儲存方式,它的本質就是map,就是key對value的關聯

  • 簡單:宣告與結構都比較簡單
  • 無初始化方式:他沒有預設的初始化方式
  • 難以調整:Scalability比較差

Proto

這是另一種方式,但我對他不太了解

  • 結構化:有專門的宣告與架構檔案
  • 初始化:有預設初始化方式
  • 容易擴展
  • 使用較困難:整體用起來難度跟Room DB應該差不多

但因為這邊是新手入門且非常小型的專案,我沒有考慮什麼scalable的問題,更沒有什麼code好不好懂的問題,先做出一個明確能跑的應用,之後就能開始鑽研各個細節

結構

既然要交紹Preference Datastore,就先了解他的架構

Dependency

implementation("androidx.datastore:datastore-preferences:1.1.7")

只要這個就好了

宣告

先另外建一個檔案(我選擇settingpage旁邊)

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

Kotlin語法糖:在Class後面用.xxx接上可以為class補充函數或屬性,你可以在非class的scope複寫新增非final宣告的函數或類別,在此dataStore只是一個屬性名稱

在一個檔案裡面只能有一個生效的datastore
宣告一個屬性(Android官方程式)

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

我們會使用到的是EXAMPLE_COUNTER,但是expample_counter才是真正的識別方式,如果更改了name屬性,就無法對到原本的屬性設定

讀取

val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}

因為這個datastore沒有null safety,所以要自己排除

寫入

suspend fun incrementCounter() {
// context = LocalContext.Current
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}

因為他是suspend fun跟database一樣,所以一樣要開Coroutine

實踐

我試過了一些宣告與使用方式,雖然無法大量減少使用的複雜度,但還是有一點心得的
如果要儲存map、list之類的也是可以,但不建議,因為需要轉換成字串在轉換回來

集中宣告

使用object讓資料聚集,方便查找

sealed class AppPreference<T>(  
    val key: Preferences.Key<T>,  
    val defaultValue: T  
) {  
    open class Str(key: Preferences.Key<String>) : AppPreference<String>(key, "")  
    open class Bool(key: Preferences.Key<Boolean>) : AppPreference<Boolean>(key, false)  
    open class IntPref(key: Preferences.Key<Int>) : AppPreference<Int>(key, 0)  
    object FINISHED_DESC : AppPreference.Bool(booleanPreferencesKey("finished_desc"))  
}

我大概解釋一下,他是一個可以存key和default value的class,之後有幾個初始化class快速初始化

泛型存取

使用泛型,讓基本的屬性查找不用寫很長一段的Conetext和map,可以簡化成getValueFlow().collectAsState()

object DataStoreManager {  
    lateinit var dataStore: DataStore<Preferences>  
  
    fun <T> getValueFlow(key: Preferences.Key<T>, default: T): Flow<T> =  
        dataStore.data.map { it[key] ?: default }  
  
    suspend fun <T> setValue(key: Preferences.Key<T>, value: T) {  
        dataStore.edit { it[key] = value }  
    }
}

初始化

基於它的特性,如果這個鍵值之前沒有值的話就是null,所以可以在object建立的時候對每一個鍵值掃描null,然後賦值,也是因為如此,我才認為這個方式很不scalable
但是如果用我的那個方式,只需要將所有的鍵值建成一個list,然後使用default value初始化就可以
這邊多加一個沒用的屬性展示

val prefKeyList = listOf<AppPreference<*>>(  
    AppPreference.FINISHED_DESC,  
    AppPreference.NOTHING_INT  
)  
  
fun init() {  
    CoroutineScope(Dispatchers.IO).launch {  
        for (pref in prefKeyList) {  
            dataStore.edit { prefs ->  
                @Suppress("UNCHECKED_CAST")  
                val p = pref as AppPreference<Any>  
                prefs[p.key] = prefs[p.key] ?: p.defaultValue  
            }  
        }  
    }  
}

Kotlin語法:由於函數不能確認pref是不是有型別綁定(int key to int value),所以要cast一下,讓編譯器閉嘴


上一篇
Day 23:替換ViewModel的資料邏輯
下一篇
Day 25:將DataStore套用在應用上
系列文
現代Android jetpack compose開發入門25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言