github blog
qq: 2383518170
wx: lzyprime
Lambda. :
After several revisions. The initial approach to DataStore encapsulation, while still unsatisfactory, is the best one we can think of. Wait until you have a new idea.
Currently:
// key
val UserId = stringPreferencesKey("user_id")
// use:
val userId = DS[UserId] / / value
DS[UserId] = "new user id" / / set value
Copy the code
// or delegate read and write:
var userId by UserId(defaultValue = "") // You need to set the default value when the property is null. userId: String
repo.login(userId)
userId = "new user id"
Copy the code
// or readOnly
val userId by UserId // read-only userId: String?
if(! userId.isNullOrEmpty()) repo.login(userId)Copy the code
DS
:
// DS:
@JvmInline
value class DSManager(private val dataStore: DataStore<Preferences>) : DataStore<Preferences> {
...
}
val DS: DSManager bylazy {... }Copy the code
Two sets of acquisition methods are provided. One is an access style like Map. One way is by attribute delegation. Operator overloading, attribute delegation, inline classes (for narrowing function scope)
Record how it went from bad to worse.
DataStore API
DataStore document
The current 1.0.0 DataStore is intended to replace the previous SharedPreference and address many of its problems. In addition to Preference’s simple key-value form, there are protobuf versions. However, small key-value data is enough, while big data suggests Room to handle database. So something in between, or really typed, is there?
DataStore provides data as a Flow, so running in a coroutine does not block the UI.
interface
DataStore interface is very simple, one data, one 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)}}}Copy the code
Data: Flow < Preferences >. Preferences can be thought of as Map
At the same time, to facilitate data modification, provide an edit extension function, call is updateData function.
For instance
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "datastore_name")
Copy the code
PreferencesDataStore provides read-only delegates only for properties in the Context: ReadOnlyProperty
>.
So it has to be the extension of the Context, and it doesn’t have to be this, val context.ds by… Can also.
This line of code should be understood by understanding Kotlin’s attribute delegate and extended attribute.
PreferencesDataStore creates a preferences_pb file to save data.
Preferences.Key
public abstract class Preferences internal constructor() {
public class Key<T> internal constructor(public valname: String){ ... }}//create:
val USER_ID = stringPreferencesKey("user_id")
val Guide = booleanPreferencesKey("guide")
Copy the code
They are internal, so you can’t call them outside. We then use stringPreferencesKey(name: String) to create a Key of a specific type. The advantage is that we can’t create a Key that does not support a type, such as Key
, Key
>.
At the same time, the Preferences.Key
is used to ensure type safety and make it clear that T type data is stored. SharedPreference, on the other hand, can flush out the previous value type:
SharedPreference.edit{
it["userId"] = 1
it["userId"] = "new user id"
}
Copy the code
Use:
/ / value -- -- -- -- -- -- -- -- -- -- -- -- --
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()
}
// Update the value ------------
anyCoroutineScope.launch {
context.dataStore.edit {
it[USER_ID] = "new user id"}}Copy the code
Flow
Take the current value flow.first () in the coroutine, or listen for changes in real time. You can also runBlocking. Of course this will have the same effect as SharedPreference, clogging the UI and causing it to stall or crash. Especially the first time you evaluate it in data, the file takes a little bit of time to read. So it can be preheated during initialization:
anyCoroutineScope.launch { context.dataStore.data.first() }
Copy the code
Encapsulation process
as Map
The most common way to do this is to assign a value to something. So it’s easy to think of overloading operator get,operator set. The brackets [] operator:
suspend operator fun <T> DataStore<Preferences>.get(key: Preferences.Key<T>): T? = data.map{ it[key] }.first()
suspend operator fun <T> DataStore<Preferences>.set(key: Preferences.Key<T>, value:T) = edit { it[key] = value }
// use:
scope.launch {
val userId = context.dataStore[UserId]
context.dataStore[UserId] = "new user id"
}
Copy the code
It looks ok, but!! Both functions do not allow suspend. Unless it is not an operator function, but a normal one: val userId = context.datastore.get (userId).
What about using runBlocking? Get is ok, but set is not ok.
So here’s the version that feeds in the CoroutineScope:
private val cache = mutablePreferencesOf()
operator fun <T> get(key: Preferences.Key<T>): T? = cache[key] ? : runBlocking {data.map { it[key] }.first() }? .also { cache[key] = it }operator fun <T> DataStore<Preferences>.set(key: Preferences.Key<T>, scope: CoroutineScope, value: T) {
cache[key] = value
scope.launch(Dispatchers.IO) { edit { it[key] = value } }
}
// use:
val userId = context.dataStore[UserId]
context.dataStore[UserId, lifecycleScope] = "new user id"
Copy the code
- Now neither of these functions need to run in the coroutine block. But because the
set
If the process is not blocked, the task is submitted. If the value is set immediately, the task may not be executed in a timely manner, resulting in a failure or an error. It addedcache: MutablePreference
And also optimize itget
Speed of operation. set
It’s so ugly, andCoroutineScope
Generally, it lives for the lifetime of a View, and when the View dies,set
The operation is cancelled. It is common to kill the View immediately after the value is exhausted, such as during login, save the value after successful login, and then jump to the main page. So this task should be mentionedApplication
The level ofCoroutineScope
In the.
And then there is:
// UnsplashApplication.kt
@HiltAndroidApp
class UnsplashApplication : Application() {
@ApplicationScope
@Inject
lateinit var applicationScope: CoroutineScope
init {
instance = this
}
companion object {
private lateinit var instance: UnsplashApplication
operator fun getValue(ref: Any? , property:KProperty< * >): UnsplashApplication = instance
}
}
// DS.kt
private val application by UnsplashApplication
operator fun <T> DataStore<Preferences>.set(key: Preferences.Key<T>, value: T) {
cache[key] = value
application.applicationScope.launch { edit { it[key] = value } }
}
Copy the code
But there’s still a problem: instead of going through set, you go through updateData. The cache is not updated, and the get/set file writes import to package_path.get, polluting the environment.
So narrow the scope with inline classes and update the cache when updateData is updated:
@JvmInline
value class DSManager(private val dataStore: DataStore<Preferences>) : DataStore<Preferences> {
override val data: Flow<Preferences> get() = dataStore.data
init {
application.applicationScope.launch { cache += data.first() }
}
override suspend fun updateData(transform: suspend (t: Preferences) - >Preferences): Preferences {
transform(cache)
return dataStore.updateData(transform)
}
operator fun <T> set(key: Preferences.Key<T>, value: T){... }operator fun <T> get(key: Preferences.Key<T>): T? =...suspend operator fun invoke(transform: suspend (MutablePreferences) - >Unit) = edit(transform)
}
val DS by lazy {
DSManager(application.dataStore)
}
// operator invoke use:
DS {
it -= UserId
it[Sig] = "xxx"
}
val userId = DS[UserId]
DS[UserId] = "new user id"
Copy the code
At the same time, the invoke, or parenthesis () operator, was used to provide the Edit operation, resulting in the current version. But there are still risks, or cache problems, not warm up in time and so on. By saving copies, you run the risk of data inconsistencies.
You can optionally kill the cache, do not provide set, get throws Flow
back, and application does not need to stand globally.
Implement the DataStore
interface with an inline class, or a normal class, via a delegate. The same can be done with a normal class, but do not delegate the interface. Otherwise, the behavior of super.xxx() cannot be obtained when override is performed.
Common class handling:
class DSManager(context: Context): DataStore<Preferences> by context.dataStore {
operator fun <T> get(key: Preferences.Key<T>): Flow<T? > =data.map{ it[key] }
suspend operator fun invoke(transform: suspend (MutablePreferences) - >Unit) = edit(transform)
}
val DS by lazy {
val application by UnsplashApplication
DSManager(context)
}
// use
val userIdFlow = DS[UserId]
scope.launch { val userId = userIdFlow.first() }
Copy the code
value by delegate
If the scenario is stricter, DataStore only provides access to a specified key-value and does not allow user-defined keys.
There’s no limit to how you can access it, so someone else can define a Key in any file and assign it to it.
Easy to do: singleton, DataStore private, which means to expose variables you want to expose. And the cache won’t be a problem. Make the exposed value an extended property of the CoroutineScope, or continue using applicationScope. It can also control whether it is writable and whether it can be deleted:
object DS {
private val ds by lazy {
val application by UnsplashApplication
application.dataStore
}
private val cache = mutablePreferencesOf()
private val UserId = stringPreferencesKey("user_id")
var CoroutineScope.userId: String
get() = cache[UserId] ? : runBlocking { ds.data.map { it[UserId].orEmpty() }.first() }
set(value) {
cache[UserId] = value
launch { ds.edit { it[UserId] = value } }
}
// or
var userId: String
get() = cache[UserId] ? : runBlocking { ds.data.map { it[UserId].orEmpty() }.first() }
set(value) {
cache[UserId] = value
val application by UnsplashApplication
application.applicationScope.launch { ds.edit { it[UserId] = value } }
}
// Delete:
varUserId: String?get() = cache[UserId] ? : runBlocking { ds.data.map { it[UserId] }.first() }
set(value) {
val application by UnsplashApplication
if(value == null) {
cache[UserId] = value
application.applicationScope.launch { ds.edit { it -= UserId } }
} else {
cache[UserId] = value
application.applicationScope.launch { ds.edit { it[UserId] = value } }
}
}
}
Copy the code
You can. But it would be a waste to write such a duplicate for each variable. This is not what happens when a property delegates to the compiler. So make it a commission:
object DS {
private val ds by lazy {
val application by UnsplashApplication
application.dataStore
}
private val cache = mutablePreferencesOf()
private fun <T> safeKeyDelegate(key: Preferences.Key<T>, defaultValue: T) =
object: ReadWriteProperty<Any? , T> {override fun getValue(thisRef: Any? , property:KProperty< * >): T = cache[key] ? : runBlocking { ds.data.map { it[key] ? : defaultValue }.first() }override fun setValue(thisRef: Any? , property:KProperty<*>, value: T) {
cache[key] = value
application.applicationScope.launch { ds.edit { it[key] = value } }
}
}
var userId: String by safeKeyDelegate(stringPreferencesKey("user_id"), "")}Copy the code
ReadWriteProperty
let’s do that. Deletable: ReadWriteProperty
, limit visibility: ReadWriteProperty
.
Use:
login(DS.userId)
DS.userId = "new user id"
Copy the code
key by delegate
You can specify a Key and use it to fetch a value from the DataStore:
operator fun <T> Preferences.Key<T>.invoke(defaultValue: T) = object: ReadWriteProperty<Any? , T> {override fun getValue(thisRef: Any? , property:KProperty< * >): T = cache[key] ? : runBlocking { ds.data.map { it[key] ? : defaultValue }.first() }override fun setValue(thisRef: Any? , property:KProperty<*>, value: T) {
cache[key] = value
application.applicationScope.launch { ds.edit { it[key] = value } }
}
}
operator fun <T> Preferences.Key<T>.provideDelegate(ref: Any? , property:KProperty< * >) = object: ReadWriteProperty<Any? , T? > {override fun setValue(thisRef: Any? , property:KProperty<*>, value: T?). {
if(value == null) {... }else {...}
}
override fun getValue(thisRef: Any? , property:KProperty< * >): T? =... }private val UserId = stringPreferencesKey("user_id")
Copy the code
Use:
// There are default values, read and write, var
var userId by UserId("default value")
login(userId)
userId = "xxx"
// There are default values, read-only, val
val userId by UserId("default value")
// No default value (deletable), read/write, var
var userId:String? by UserId
// No default value (deletable), read-only, val
val userId:String? by UserId
Copy the code
~ lambda. :
About these routines, according to the actual situation of the project, choose the encapsulation mode. Permutations and combinations.