github blog
qq: 2383518170
wx: lzyprime
λ:
當(dāng)前 DataStore 1.0.0
。
DataStore
的封裝已經(jīng)試過好多方式。仍不滿意它抱。大概總結(jié)一下路數(shù):
為
DataStore<Preferences>
提供[]
訪問断国。通過
getValue, setValue
實(shí)現(xiàn)委托構(gòu)造页眯。利用
()
運(yùn)算符加suspend
, 從而實(shí)現(xiàn)掛起效果赊锚。
這里最大的限制是[]
, getValue, setValue
是不能加suspend
的。所以要么傳CoroutineScope
進(jìn)來赁还,要么加runBloacking
并齐。但runBlocking
就喪失了DataStore
的優(yōu)勢漏麦,退化成 SharedPreference
.
// api preview
val kUserId = stringPreferencesKey("user_id")
// 1.
val userId: String? = anyDataStore[kUserId]
val userId: String = anyDataStore[kUserId, "0"]
anyDataStore[kUserId] = "<new value>"
// 2.
var userId: String by anyDataStore(...)
userId = "<new value>"
DataStore API
當(dāng)前DataStore 1.0.0
,目的是替代之前的SharedPreference
, 解決它的諸多問題况褪。除了Preference簡單的key-value
形式撕贞,還有protobuf
版本。但是感覺雞肋测垛,小數(shù)據(jù)key-value
就夠了捏膨,大數(shù)據(jù)建議Room
處理數(shù)據(jù)庫。所以介于中間的部分食侮,或者真的需要類型化的号涯,真的有嗎?
DataStore
以Flow
的方式提供數(shù)據(jù)锯七,所以跑在協(xié)程里链快,可以不阻塞UI。
interface
DataStore
的接口非常簡單眉尸,一個data
, 一個fun updateData
:
// T = Preferences
public interface DataStore<T> {
public val data: Flow<T>
public suspend fun updateData(transform: suspend (t: T) -> T): T
}
public suspend fun DataStore<Preferences>.edit(transform: suspend (MutablePreferences) -> Unit): Preferences {
return this.updateData { it.toMutablePreferences().apply { transform(this) } }
}
data: Flow<Preferences>
域蜗。 Preferences
可以看作是個Map<Preferences.Key<*>, Any>
。
同時為了數(shù)據(jù)修改方便效五,提供了個edit
的拓展函數(shù)地消,調(diào)用的就是updateData
函數(shù)。
獲取實(shí)例
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "datastore_name")
preferencesDataStore
只為Context
下的屬性提供只讀的委托:ReadOnlyProperty<Context, DataStore<Preferences>>
畏妖。
所以前邊非要定成Context
的拓展屬性,屬性名不一定非是這個, val Context.DS by ...
也可以疼阔。
搞清楚kotlin的屬性委托
和拓展屬性
戒劫,就懂了這行代碼半夷。
preferencesDataStore
相當(dāng)于創(chuàng)建了個fileDir/<datastore_name>.preferences_pb
的文件, 存數(shù)據(jù)。
Preferences.Key
public abstract class Preferences internal constructor() {
public class Key<T> internal constructor(public val name: String){ ... }
}
//create:
val USER_ID = stringPreferencesKey("user_id")
val Guide = booleanPreferencesKey("guide")
都被加了internal
限制迅细,所以在外邊調(diào)不了構(gòu)造巫橄。然后通過stringPreferencesKey(name: String)
等一系列函數(shù),創(chuàng)建特定類型的Key
茵典, 好處是限定了類型的范圍湘换,不會創(chuàng)出不支持類型的Key
, 比如Key<UserInfo>
,Key<List<*>>
统阿。
同時通過Preferences.Key<T>
保證類型安全彩倚,明確存的是T
類型數(shù)據(jù)。而SharedPreference
, 可以沖掉之前的值類型:
SharedPreference.edit{
it["userId"] = 1
it["userId"] = "new user id"
}
使用:
// 取值 -------------
val userIdFlow: Flow<String> = context.dataStore.data.map { preferences ->
// No type safety.
preferences[USER_ID].orEmpty()
}
anyCoroutineScope.launch {
repo.login(userIdFlow.first())
userIdFlow.collect {
...
}
}
// or
val userId = runBlocking {
userIdFlow.first()
}
// 更新值 ------------
anyCoroutineScope.launch {
context.dataStore.edit {
it[USER_ID] = "new user id"
}
}
Flow<Preference>.map{}
流轉(zhuǎn)換扶平, 在preference
這個 "Map" 里取出UserId
的值帆离,有可能沒有值。得到一個Flow<T>
结澄。
在協(xié)程里取當(dāng)前值Flow.first()
, 或者實(shí)時監(jiān)聽變化哥谷。也可以runBlocking
變成阻塞式的。當(dāng)然這就會和SharedPreference
一樣的效果麻献,阻塞UI, 導(dǎo)致卡頓或崩潰们妥。尤其是第一次在data
中取值,文件讀入會花點(diǎn)時間勉吻。所以可以在初始化時王悍,預(yù)熱一下:
anyCoroutineScope.launch { context.dataStore.data.first() }
封裝過程
[]
操作符
1. return Flow<T?> || Flow<T>
由于get set
函數(shù)無法加 suspend
, 所以get
只能以Flow
的形式返回值. 而如果想實(shí)現(xiàn)set
的效果,就要runBlocking
餐曼, 這樣DataStore
就失去了優(yōu)勢压储。
operator fun <T> DataStore<Preferences>.get(key: Preferences.Key<T>): Flow<T?> = data.map{ it[key] }
operator fun <T> DataStore<Preferences>.get(key: Preferences.Key<T>, defaultValue: T): Flow<T> = data.map{ it[key] }
// operator fun <T> DataStore<Preferences>.set(key: Preferences.Key<T>, value: T?) = runBlocking {
// edit { if(value != null) it[key] = value else it -= key }
// }
// use:
val userId: Flow<String?> = anyDataStore[kUserId]
val userId: Flow<String> = anyDataStore[kUserId, ""]
// anyDataStore[kUserId] = "<new value>"
2. 為了解決set
, 有了把CoroutineScope
傳進(jìn)來的版本:
但是由于set
過程不阻塞,如果立刻取值源譬,可能任務(wù)執(zhí)行的不及時集惋,導(dǎo)致取到的是舊值。 而且如果scope
生命結(jié)束仍沒執(zhí)行完踩娘,則保存失敗刮刑。
operator fun <T> DataStore<Preferences>.set(key: Preferences.Key<T>, scope: CoroutineScope, value: T?) {
scope.launch {
edit { if(value != null) it[key] = value else it -= key }
}
}
// use:
anyDataStore[kUserId, anyScope] = "<new value>"
3. 包裹DataStore
, 加cache
優(yōu)化。
加入cache
處理更新不及時問題养渴,但有可能 預(yù)熱DataStore
操作不及時雷绢,導(dǎo)致cache
錯亂。 get
使用了runBlocking
理卑,仍有隱患翘紊。
class DS(
private val dataStore: DataStore<Preferences>,
private val scope: CoroutineScope,
) {
private val cache = mutablePreferencesOf()
init {
// 預(yù)熱 DataStore
scope.launch {
cache += dataStore.data.first()
}
}
operator fun <T> get(key: Preferences.Key<T>): T? =
cache[key] ?: runBlocking {
dataStore.data.map { it[key] }.first()?.also { cache[key] = it }
}
operator fun <T> set(key:Preferences.Key<T>, value:T?) {
if(value != null) cache[key] = value
scope.launch {
dataStore.edit { if(value != null) it[key] = value else it -= key }
}
}
companion object {
private const val STORE_NAME = "global_store"
private val Context.dataStore by preferencesDataStore(STORE_NAME)
}
}
// use:
// val ds: DS // 依賴注入或instance拿到單例
val userId = ds[kUserId]
ds[kUserId] = "<new value>"
總之
[]
難解決的是runBlocking
執(zhí)行。
value class
, ()
操作符
- 內(nèi)聯(lián)類限定對
DataStore
的訪問藐唠。[]
只提供get
操作帆疟,返回Flow
鹉究。 - 通過
()
操作符暴露DataStore<T>.edit
.
@JvmInline
value class DS(private val dataStore: DataStore<Preferences>) {
operator fun <T> get(key: Preferences.Key<T>) =
dataStore.data.map { it[key] }
suspend operator fun invoke(block: suspend (MutablePreferences) -> Unit) =
dataStore.edit(block)
companion object {
private const val STORE_NAME = "global_store"
private val Context.dataStore by preferencesDataStore(STORE_NAME)
}
}
// use
val userId = ds[kUserId]
suspend {
ds {
it[kUserId] = "<new value>"
it -= kUserId
}
}
屬性委托
abstract class PreferenceItem<T>(flow: Flow<T>) : Flow<T> by flow {
abstract suspend fun update(v: T?)
}
operator fun <T> DataStore<Preferences>.invoke(
buildKey: (name: String) -> Preferences.Key<T>,
defaultValue: T,
) = ReadOnlyProperty<Any?, PreferenceItem<T>> { _, property ->
val key = buildKey(property.name)
object : PreferenceItem<T>(data.map { it[key] ?: defaultValue }) {
override suspend fun update(v: T?) {
edit {
if (v == null) {
it -= key
} else {
it[key] = v
}
}
}
}
}
// use
val userId: PreferenceItem<String> by anyDataStore(::stringPreferencesKey, "0")
suspend {
userId.update("<new value>")
}
Preferences.Key<T>
可以通過判別 T
的類型然后選擇對應(yīng)構(gòu)造函數(shù),匹配失敗拋異常踪宠。