1. 簡介
DataStore是Google Android Jetpack組件新推出的數(shù)據(jù)存儲解決方案避凝,其主要優(yōu)點如下:
- 允許使用Protocol-Buffers存儲鍵值對或類型化對象
- 使用Kotlin協(xié)程和Flow來異步故黑、一致和事務(wù)性地存儲數(shù)據(jù)
DataStore并不被建議用來存儲大量復(fù)雜的數(shù)據(jù),并且無法局部的更新數(shù)據(jù),如果有類似的需求可以使用Room組件替代荒勇。
由于使用了Kotlin協(xié)程和Flow相關(guān)的知識,所以建議在使用之前先在Kotlin協(xié)程與Flow官方文檔進(jìn)行了解。(注:英語不好的可以翻譯或者搜索相關(guān)中文教程)
2. Preferences DataStore 與 Proto DataStore
可以這樣簡單的理解兩者的區(qū)別:
Preferences DataStore與SharedPreferences類似压储,通過鍵值對存儲數(shù)據(jù),不保證類型安全源譬。
Proto DataStore通過Protocol-Buffers定義存儲數(shù)據(jù)類型以及結(jié)構(gòu)渠脉,保證類型安全。
注:本文只介紹Preferences DataStore的使用方式瓶佳,因為這足夠滿足多數(shù)情況下的使用了芋膘。如果想要進(jìn)一步了解Proto DataStore,建議前往DataStore官方教程與Protocol-Buffers官方教程查看最新文檔霸饲。
3. 依賴導(dǎo)入(按需導(dǎo)入)
DataStore API更新動態(tài)與最新版本查詢
dependencies {
// Typed DataStore (Proto DataStore)
implementation "androidx.datastore:datastore:1.0.0"
// Typed DataStore (沒有Android依賴項为朋,包含僅適用于 Kotlin 的核心 API)
implementation "androidx.datastore:datastore-core:1.0.0"
// 可選 - RxJava2 支持
implementation "androidx.datastore:datastore-rxjava2:1.0.0"
// 可選 - RxJava3 支持
implementation "androidx.datastore:datastore-rxjava3:1.0.0"
// Preferences DataStore(可以直接使用)
implementation "androidx.datastore:datastore-preferences:1.0.0"
// Preferences DataStore (沒有Android依賴項,包含僅適用于 Kotlin 的核心 API)
implementation "androidx.datastore:datastore-preferences-core:1.0.0"
// 可選 - RxJava2 支持
implementation "androidx.datastore:datastore-preferences-rxjava2:1.0.0"
// 可選 - RxJava3 支持
implementation "androidx.datastore:datastore-preferences-rxjava3:1.0.0"
}
注1:2021.1.15 自alpha06開始修改了Preference.Key的API厚脉,本文已更新
注2:2021.2.24 自alpha07開始廢棄了Context.createDataStore的API习寸,本文已更新
注3:2021.8.4 DataStore 1.0.0 release
4. Preferences DataStore 入門
4.1 初始化DataStore
官方示例(創(chuàng)建名稱為settings的DataStore):
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
根據(jù)官方注釋的說明,該操作用于創(chuàng)建SingleProcessDataStore的實例傻工,用戶負(fù)責(zé)確保一次操作一個文件的SingleProcessDataStore的實例永遠(yuǎn)不會超過一個霞溪。
如果使用RxJava的話需要使用RxPreferenceDataStoreBuilder替代
因此為了防止出錯,方便管理中捆,個人建議使用單例模式進(jìn)行DataStore實例的管理鸯匹,但是由于需要使用Context對象才能夠?qū)嵗钥梢酝ㄟ^使用Application的靜態(tài)context變量的方式實現(xiàn)泄伪。
因為DataStore必須使用by委托的方式創(chuàng)建殴蓬,所以在非Context類下創(chuàng)建較為麻煩,因此最好使用Application的靜態(tài)Context方式作為媒介創(chuàng)建DataStore蟋滴。
// App.kt
class App : Application() {
companion object {
lateinit var instance: App
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
// SettingsDataStore.kt
object {
// 創(chuàng)建DataStore
private val App.dataStore: DataStore<Preferences> by createDataStore(
name = "settings"
)
// 對外開放的DataStore變量
val dataStore = App.instance.dataStore
}
創(chuàng)建的DataStore存儲文件將會被放置在 "/data/data/{包名}/files/datastore/{DataStore名稱}.preferences_pb"
4.2 鍵(Key)創(chuàng)建
官方示例(創(chuàng)建名為example_counter的Int類型的鍵):
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
通過preferencesKey可以創(chuàng)建的數(shù)據(jù)類型為:Int染厅,String,Boolean津函,F(xiàn)loat肖粮,Long,Double尔苦。
如果想要創(chuàng)建Set<T>類型的鍵涩馆,必須使用以下方法:
val EXAMPLE_COUNTER_SET = stringSetPreferencesKey("example_counter_set")
通過preferencesSetKey可以創(chuàng)建的數(shù)據(jù)類型目前僅支持String行施。
如果希望能夠?qū)⒆兞棵鳛殒I名,可以使用如下方法建立委托方法:
fun booleanPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Boolean>> { _, property -> booleanPreferencesKey(property.name) }
fun stringPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<String>> { _, property -> stringPreferencesKey(property.name) }
fun intPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Int>> { _, property -> intPreferencesKey(property.name) }
fun longPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Long>> { _, property -> longPreferencesKey(property.name) }
fun floatPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Float>> { _, property -> floatPreferencesKey(property.name) }
fun doublePreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Double>> { _, property -> doublePreferencesKey(property.name) }
fun stringSetPreferencesKey() = ReadOnlyProperty<Any, Preferences.Key<Set<String>>> { _, property -> stringSetPreferencesKey(property.name) }
這樣就可以通過以下方式實現(xiàn)鍵的創(chuàng)建:
val example_counter by intPreferencesKey()
4.3 數(shù)據(jù)讀取
官方示例(讀取EXAMPLE_COUNTER鍵的值凌净,若為null即不存在,則使用0作為默認(rèn)值):
val exampleCounterFlow: Flow<Int> = dataStore.data.map { preferences ->
// 無類型安全
preferences[EXAMPLE_COUNTER] ?: 0
}
dataStore.data本質(zhì)上返回的是一個Flow<Preference>對象屋讶,此處的Preference僅能夠進(jìn)行讀取操作冰寻,接著通過Flow提供的map方法轉(zhuǎn)換接下來傳遞的數(shù)據(jù)。
如果想要一次性讀取多個數(shù)據(jù)皿渗,或者讀取數(shù)據(jù)為一個data class斩芭,可以采用如下方式:
data class Example(val value_1: Int, val value_2: String?)
val key_1 = intPreferencesKey("key_1")
val key_2 = stringPreferencesKey("key_2")
val exampleFlow: Flow<Example> = dataStore.data.map { preferences ->
Example(preferences[key_1] ?: 0, preferences[key_2])
}
DataStore會使用內(nèi)存緩存的方式加快同一數(shù)據(jù)二次讀取速度,因此多數(shù)情況下并不需要手動設(shè)置緩存相關(guān)的代碼乐疆。
通過Flow API划乖,實際讀取到數(shù)據(jù)可以主要通過以下兩種方式:
// 需要在協(xié)程函數(shù)內(nèi)部或suspend函數(shù)下運行,僅讀取一次最新數(shù)據(jù)
exampleFlow.first()
// 需要在協(xié)程函數(shù)內(nèi)部或suspend函數(shù)下運行挤土,會監(jiān)聽數(shù)據(jù)變化并返回最新數(shù)據(jù)
exampleFlow.collect { data ->
println(data)
}
4.4 數(shù)據(jù)修改
官方示例(對EXAMPLE_COUNTER鍵的值進(jìn)行從0開始的自增):
suspend fun incrementCounter() {
dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
}
DataStore提供的edit()方法可以將傳入的操作視作單個事務(wù)進(jìn)行修改琴庵,因此滿足了數(shù)據(jù)的一致和事務(wù)性。
示例的lambda函數(shù)中傳入的settings為MutablePreferences對象仰美,提供了數(shù)據(jù)的讀取與修改操作迷殿。
對于數(shù)據(jù)較大的批量的修改,建議可以合并到一個事務(wù)內(nèi)進(jìn)行以提高IO效率咖杂。
4.5 異步
由于DataStore使用了Kotlin提供的Flow作為數(shù)據(jù)獲取的方式庆寺,因此滿足的IO操作異步的需求。但是并非所有的IO操作都可以立即遷移為異步執(zhí)行诉字,所以官方文檔中指出可以使用以下方法臨時解決問題:
// 普通的堵塞方式讀取數(shù)據(jù)懦尝,可能會導(dǎo)致死鎖,最好別用
val exampleData = runBlocking { dataStore.data.first() }
// 在LifeCycle提供的協(xié)程方法中讀取數(shù)據(jù)
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
dataStore.data.first()
// 可以在此處理 IOException
}
}
5. 從SharedPreferences中遷移
示例代碼:
val dataStore = context.preferenceDataStore(
name = "{DataStore名稱}",
migrations = listOf(SharedPreferencesMigration(context, "{SharedPreferences名稱}"))
)
默認(rèn)情況下完成遷移后將會刪除原始SharedPreferences的xml文件壤圃,可以通過參數(shù)調(diào)整陵霉。
注:此處的SharedPreferencesMigration并非該類的原始構(gòu)造方法,而是androidx.datastore.preferences包下的kotlin函數(shù)伍绳。
6. 在PreferenceFragmentCompat中使用DataStore
Google目前已經(jīng)在PreferenceFragment上提供可以使用其他數(shù)據(jù)源的兼容性接口撩匕,首先手動實現(xiàn)基于DataStore的抽象類androidx.preference.PreferenceDataStore,然后在PreferenceFragmentCompat中獲取PreferenceManager墨叛,最后通過PreferenceManager的以下方法就可以將默認(rèn)的SharedPreference存儲方式替換為DataStore了止毕。
public void setPreferenceDataStore(PreferenceDataStore dataStore)
注:雖然抽象類名字為PreferenceDataStore,但是本身與DataStore并沒有關(guān)系
以下為筆者實現(xiàn)的PreferenceDataStore
open class DataStorePreferenceAdapter(private val dataStore: DataStore<Preferences>, scope: CoroutineScope) : PreferenceDataStore() {
private val prefScope = CoroutineScope(scope.coroutineContext + SupervisorJob() + Dispatchers.IO)
private val dsData = dataStore.data.shareIn(prefScope, SharingStarted.Eagerly, 1)
private fun <T> putData(key: Preferences.Key<T>, value: T?) {
prefScope.launch {
dataStore.edit {
if (value != null) it[key] = value else it.remove(key)
}
}
}
private fun <T> readNullableData(key: Preferences.Key<T>, defValue: T?): T? {
return runBlocking(prefScope.coroutineContext) {
dsData.map {
it[key] ?: defValue
}.firstOrNull()
}
}
private fun <T> readNonNullData(key: Preferences.Key<T>, defValue: T): T {
return runBlocking(prefScope.coroutineContext) {
dsData.map {
it[key] ?: defValue
}.first()
}
}
override fun putString(key: String, value: String?) = putData(stringPreferencesKey(key), value)
override fun putStringSet(key: String, values: Set<String>?) = putData(stringSetPreferencesKey(key), values)
override fun putInt(key: String, value: Int) = putData(intPreferencesKey(key), value)
override fun putLong(key: String, value: Long) = putData(longPreferencesKey(key), value)
override fun putFloat(key: String, value: Float) = putData(floatPreferencesKey(key), value)
override fun putBoolean(key: String, value: Boolean) = putData(booleanPreferencesKey(key), value)
override fun getString(key: String, defValue: String?): String? = readNullableData(stringPreferencesKey(key), defValue)
override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? = readNullableData(stringSetPreferencesKey(key), defValues)
override fun getInt(key: String, defValue: Int): Int = readNonNullData(intPreferencesKey(key), defValue)
override fun getLong(key: String, defValue: Long): Long = readNonNullData(longPreferencesKey(key), defValue)
override fun getFloat(key: String, defValue: Float): Float = readNonNullData(floatPreferencesKey(key), defValue)
override fun getBoolean(key: String, defValue: Boolean): Boolean = readNonNullData(booleanPreferencesKey(key), defValue)
}
7. 總結(jié)
相比于漏洞百出到就連Google都不想修復(fù)的SharedPreferences漠趁,DataStore確實提供了一套簡單可用的異步數(shù)據(jù)存儲方案扁凛,不管是Kotlin協(xié)程還是Flow,都極大程度的提高了使用的體驗闯传。
與騰訊已經(jīng)開源并穩(wěn)定使用的MMKV相比谨朝,使用官方組件最大的好處就是與其他組件的相互兼容性,并且如果已經(jīng)使用了Kotlin協(xié)程庫,使用DataStore可以減少App的體積字币。
目前DataStore已經(jīng)release则披,總體使用效果還是不錯的。不過如果項目大都是靜態(tài)存儲的數(shù)據(jù)(不需要觀察數(shù)據(jù)更新)或者沒有任何多進(jìn)程等同步的需求洗出,那么也沒必要馬上遷移到DataStore中士复。不過DataStore依然存在的一個問題就是無法直觀的看到與修改已經(jīng)存放的數(shù)據(jù),這需要Android Studio后續(xù)的更新支持翩活。
Made By XFY9326