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

, Any>.

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

. Map {} Flow . Map {} Flow . We get a 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 thesetIf 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: MutablePreferenceAnd also optimize itgetSpeed of operation.
  • setIt’s so ugly, andCoroutineScopeGenerally, it lives for the lifetime of a View, and when the View dies,setThe 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 mentionedApplicationThe level ofCoroutineScopeIn 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.