Cause SharedPreferences

SharedPreferences (SP) SharedPreferences (SP) SharedPreferences (SP) SharedPreferences (SP) SharedPreferences (SP)

val sp = getSharedPreferences("Test", Context.MODE_PRIVATE)
sp.edit { putString("jetPack"."text")}val jetPack = sp.getString("jetpack"."")
Copy the code

With only a few lines of code above, the use of SP is complete, but there are many pits behind the simple use of SP. The getXXX() method can cause the main thread to block, is not type-safe, the loaded data stays in memory, the apply() method is asynchronous, ANR can occur, cannot be used for cross-process communication, and so on… The specific reason for the pit is not described here, we can jump directly to the above article to view.

One might ask, why would you write another article on how to use DataStore after the previous one? Because… After reading this article, I tried to feel that it was a little troublesome to use, and there were some problems when using it, which I thought everyone might also encounter. Besides, I did not find the results I wanted after searching on Baidu, so I decided to write an article to avoid people to step on the pit repeatedly.

Embrace the DataStore

Why use it?

Let’s take a look at the official Google description of DataStore:

Storing data in an asynchronous, consistent transactional manner overcomes some of the shortcomings of SharedPreferences

We should abandon SP and embrace DataStore. Here are some of the benefits of DataStore:

  • DataStore is implemented based on Flow, ensuring security in the main thread
  • Update data is processed in a transactional manner with four characteristics (atomicity, consistency, isolation, and persistence)
  • There are no data persistence methods like apply() and commit()
  • The SharedPreferences are automatically migrated to the DataStore to ensure data consistency and prevent data damage
  • You can listen to the success or failure of an operation

Take another look at the difference between Google Analytics SharedPreferences and DataStore:

See what’s going on here? Then hurry to continue to read!

Method of use

Preference data store and stereotype data store

DataStore provides two different implementations: the preference DataStore and the Proto DataStore.

  • Proto DataStore stores data as instances of custom data types. This implementation requires the schema to be defined using a protocol buffer, but it provides type safety.

  • The preference DataStore uses keys to store and access data. This implementation does not require a predefined schema and does not provide type safety.

Add the dependent

There are two different implementations that use different dependencies, so drop them in the order above.

Proto DataStore way:

// Typed DataStore (Typed API surface, such as Proto)
dependencies {
  implementation "Androidx. Datastore: datastore: 1.0.0 - alpha05"
}
// Alternativey - use the following artifact without an Android dependency.
dependencies {
  implementation "Androidx. Datastore: datastore - core: 1.0.0 - alpha05"
}
Copy the code

Key-value pair mode:

// Preferences DataStore (SharedPreferences like APIs)
dependencies {
  implementation "Androidx. Datastore: datastore - preferences: 1.0.0 - alpha05"
}
// Alternativey - use the following artifact without an Android dependency.
dependencies {
  implementation "Androidx. Datastore: datastore - preferences - core: 1.0.0 - alpha05"
}
Copy the code

Proto DataStore is used

The Proto DataStore implementation uses DataStore and protocol buffers to persist typed objects to disk.

This article will not describe the specific use of Proto DataStore, you can go to the official documentation to check, because the use of the protobuf language, this will be skipped first, because this is only seen before, not actually used, so I will not teach you how to use the Proto DataStore.

Here is the address described in the official document:

Developer. The android. Google. Cn/topic/libra…

The key value pair mode is used specifically

Build the DataStore

val preferenceName = "PlayAndroidDataStore"
var dataStore: DataStore<Preferences> = context.createDataStore(preferenceName)
Copy the code

Write data

Int, Long, Boolean, Float, String DataStore can only write to the following fixed types: Int, Long, Boolean, Float, String

suspend fun incrementCounter(a) {
  dataStore.edit { settings ->
    valcurrentCounterValue = settings[EXAMPLE_COUNTER] ? :0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1}}Copy the code

The method should not pass in a value to write data. Then I thought, oh, the official meaning is to change the value directly to write data as I wrote in the SP example above.

Read the data

val EXAMPLE_COUNTER = preferencesKey<Int> ("example_counter")
val exampleCounterFlow: Flow<Int> = dataStore.data
  .map { preferences ->
    // No type safety.preferences[EXAMPLE_COUNTER] ? :0
}
Copy the code

This is a little bit easier to understand, getting value by key.

Is it very simple, not initialization, and then need to save the time to save, need to take when take a terrible! No need to see! When it’s used, it’s useless!

Clear data

Before we use SP, we can directly use the following method to clear data:

fun clear(context: Context) {
    val preferences = context.getSharedPreferences("name", Context.MODE_PRIVATE)
    val editor = preferences.edit()
    editor.clear()
    editor.apply()
}
Copy the code

But how does today’s DataStore clean up data? It is similar to SP, even simpler:

suspend fun clear(a) {
    dataStore.edit {
        it.clear()
    }
}
Copy the code

Migrate SP data to the DataStore

To initialize the DataStore, we call createDataStore, a context extension function. We pass in only the name of the DataStore.

public fun Context.createDataStore(
    name: String,
    corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
    migrations: List<DataMigration<Preferences>> = listOf(),
    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): DataStore<Preferences> =
    PreferenceDataStoreFactory.create(
        corruptionHandler = corruptionHandler,
        migrations = migrations,
        scope = scope
    ) {
        File(this.filesDir, "datastore/$name.preferences_pb")}Copy the code

This method has several parameters, but they all have default values.

  • Name: this is the name of the DataStore
  • CorruptionHandler: If the data store encounters a CorruptionException when it tries to read data, the corruptionHandler is called. When data cannot be deserialized, the serializer raises a CorruptionException
  • Migrations: This parameter is used to migrate SP, and is shown below
  • Scope: this parameter is more familiar: scope of coroutines

After reading the above parameters, you should already know how to migrate. Here is the migration code:

dataStore = context.createDataStore(
    name = preferenceName,
    migrations = listOf(
        SharedPreferencesMigration(
            context,
            "You store the Name of SP")))Copy the code

Isn’t that simple, but be aware that the migration will run before the data is accessed. Each Producer and migration may run multiple times, regardless of whether it has already succeeded (perhaps because of another migration failure or a failed write to the disk).

Record on pit

Small pits

The sample code from the official documentation has been written above, so try using it with a little change.

We have already initialized the Boolean, so we can use it directly.

suspend fun saveBooleanData(key: String, value: Boolean) {
    dataStore.edit { mutablePreferences ->
        mutablePreferences[preferencesKey(key)] = value
    }
}
Copy the code

This will make it easier for us to build a preferencesKey object. Programmers, save the trouble when you can!

Here’s another way to read a Boolean:

fun readBooleanFlow(key: String, default: Boolean = false): Flow<Boolean> =
    dataStore.data
        .catch {
             // When reading data and encountering an error, if it is' IOException ', send an emptyPreferences to reuse it
             // But if it is another exception, it is best to throw it away without hiding the problem
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throwit } }.map { it[preferencesKey(key)] ? : default }Copy the code

The read method is also slightly encapsulated and returns a Flow directly. Catch is added to the read method, because the IO exception in the Flow cannot be caught by try/catch, so it is needed to catch the exception in this way.

The other types of Float, Int, Long, and String are all similar, except that they have different parameter types.

The test method is very simple. Two buttons, one for each type to add data, and one for each type to read data, let’s look at the new button:

GlobalScope.launch {
    dataStore.apply {
        saveBooleanData("BooleanData".true)
        saveFloatData("FloatData".15f)
        saveIntData("IntData".12)
        saveLongData("LongData".56L)
        saveStringData("StringData"."I love you.")}}Copy the code

Here, because edit in the save method is a suspend function, it needs to be used inside the coroutine.

Let’s take a look at the read code:

GlobalScope.launch {
    Log.e("ZHUJIANG"."Ha ha ha.")
    dataStore.readBooleanFlow("BooleanData").collect {
        Log.e("ZHUJIANG"."BooleanData: $it" )
    }
    dataStore.readFloatFlow("FloatData").collect {
        Log.e("ZHUJIANG"."FloatData: $it" )
    }
    dataStore.readIntFlow("IntData").collect {
        Log.e("ZHUJIANG"."IntData: $it" )
    }
    dataStore.readLongFlow("LongData").collect {
        Log.e("ZHUJIANG"."LongData: $it" )
    }
    dataStore.readStringFlow("StringData").collect {
        Log.e("ZHUJIANG"."StringData: $it" )
    }
    Log.e("ZHUJIANG"."Hahaha 222")}Copy the code

Are there any questions about the code above? I wrote it in this way when I first used it, and I thought it was used in this way. Maybe it was due to my lack of understanding of Flow, which was the case when I used it before.

When I write here, I feel everything is going well, I feel very good, the new library is very simple, reasonable!

Let’s run it! Here is the Log from the run click:

2020-12-04 20:48:55.399 7147-7254/com.zj. Play E/ZHUJIANG: hahaha 2020-12-04 20:48:55. BooleanData: trueCopy the code

Ah? Why? There’s so much execution going on… Why is only the first one printed here? And the bottom Log didn’t come out!

Solve the potholes

I feel like I wrote something wrong, but the official document says so, I used Flow in the same way before, no, go to the document again!

And sure enough, they found the answer, according to the official description:

One of the main benefits of DataStore is the asynchronous API, but it may not always be feasible to change the surrounding code to be asynchronous. This may be the case if you are using an existing code base that uses synchronous disk I/O, or if you have dependencies that do not provide asynchronous apis.

Kotlin coroutines provide runBlocking() coroutine generators to help bridge the gap between synchronous and asynchronous code. You can use runBlocking() to synchronously read data from DataStore. The following code blocks the calling thread until the DataStore returns data

val exampleData = runBlocking { dataStore.data.first() }
Copy the code

The truth is out! The data Flow is also asynchronous. You can use first() if you want to fetch it all the time, so let’s add a new wrapper method:

fun readBooleanData(key: String, default: Boolean = false): Boolean {
    var value = false
    runBlocking {
        dataStore.data.first { value = it[preferencesKey(key)] ? : defaulttrue}}return value
}
Copy the code

This method is based on the encapsulated method above, which returns a Flow object, where the Boolean value is obtained synchronously through the first() method. Change the type as follows, write the remaining methods, and then modify the test code:

Log.e("ZHUJIANG"."Ha ha ha.")
val booleanData = dataStore.readBooleanData("BooleanData")
Log.e("ZHUJIANG"."booleanData: $booleanData" )
val floatData = dataStore.readFloatData("FloatData")
Log.e("ZHUJIANG"."floatData: $floatData" )
val intData = dataStore.readIntData("IntData")
Log.e("ZHUJIANG"."intData: $intData" )
val longData = dataStore.readLongData("LongData")
Log.e("ZHUJIANG"."longData: $longData" )
val stringData = dataStore.readStringData("StringData")
Log.e("ZHUJIANG"."stringData: $stringData" )
Log.e("ZHUJIANG"."Hahaha 222")
Copy the code

Let’s look at the printed values again:

The 2020-12-04 21:25:20. 124, 19620-19711 / com. What zj had. Play/ZHUJIANG E: Zj. play E/ZHUJIANG: booleanData True 2020-12-04 21:25:20.168 19620-19709/com.zj. Play E/ZHUJIANG: floatData: Zj. Play E/ZHUJIANG: intData: Zj. Play E/ZHUJIANG: longData: Zj. Play E/ZHUJIANG: stringData: I love you. I love youCopy the code

Either! Perfect! That’s what we wanted! So the use of DataStore is similar to the use of SP!

You can use runBlocking not only to access data, but also to store data:

fun saveSyncBooleanData(key: String, value: Boolean) =
    runBlocking { saveBooleanData(key, value) }
Copy the code

Whenever runBlocking is added, the code in the block blocks the calling thread until execution ends. Obviously, the main thread will stall due to blocking for time-consuming operations, so use asynchronous storage or reads for time-consuming operations.

There’s just one problem: DataStore is designed to support asynchrony. Flow is asynchronous. Flow is asynchronous. We can use it as well, but what is wrong with this code?

Let’s think of is stored after read directly, but the Flow is observed, it will be the current block on live coroutines, because it will change the value of the return to again, do it say not very good understanding, or another test, let’s modify the save code, to every click on the stored value is added at the same time:

saveIntData("IntData", add++)
Copy the code

Set add to a global variable, and then read the method to write only one:

dataStore.readIntFlow("IntData").collect {
    Log.e("ZHUJIANG"."IntData: $it")}Copy the code

After running, click write once, click read again, click multiple times, and then look at the typed Log:

The 2020-12-04 21:32:50. 915, 23116-23159 / com. What zj had. Play/ZHUJIANG E: 918 23116-23159/com.zj. Play E/ZHUJIANG: IntData: 23116-23433/com.zj. Play E/ZHUJIANG: IntData: Zj. Play E/ZHUJIANG: IntData: 122020-12-04 21:32:53.447 23116-23158/com.zj. Play E/ZHUJIANG: IntData: 773 23116-23432/com.zj. Play E/ZHUJIANG: IntData: 28Copy the code

It’s just that it’s blocking the coroutine because it’s waiting for data! Is there any way to stop it from waiting, or blocking? First () = first() = first() The first method obtains the data for the first time in Flow. Of course, it can also be set to collect only once:

dataStore.readBooleanFlow("StringData").take(1).collect{
    Log.e("ZHUJIANG"."StringData: $it")}Copy the code

Check the Flow method, we can also use the following method to obtain the first data:

dataStore.readIntFlow("IntData").first {
    Log.e("ZHUJIANG"."111IntData: $it")
    true
}
Copy the code

Notice that you need to return a Boolean, and that Boolean returns true, if it returns false just like collect, and the return value of this first method means true if the data is what you want, Flow ends, return false if you don’t have the data you want, and Flow continues to accept data, blocking the current coroutine.

Take a look at the source code:

public suspend fun <T> Flow<T>.first(predicate: suspend (T) - >Boolean): T {
    var result: Any? = NULL
    collectWhile {
        if (predicate(it)) {
            result = it
            false
        } else {
            true}}if (result === NULL) throw NoSuchElementException("Expected at least one element matching the predicate $predicate")
    return result as T
}
Copy the code

This method takes a function, returns a Boolean, and calls a collectWhile.

internal suspend inline fun <T> Flow<T>.collectWhile(crossinline predicate: suspend (value: T) - >Boolean) {
    val collector = object : FlowCollector<T> {
        override suspend fun emit(value: T) {
            // Note: we are checking predicate first, then throw. If the predicate does suspend (calls emit, for example)
            // the the resulting code is never tail-suspending and produces a state-machine
            if(! predicate(value)) {throw AbortFlowException(this)}}}try {
        collect(collector)
    } catch (e: AbortFlowException) {
        e.checkOwnership(collector)
    }
}
Copy the code

This method accepts a function that returns a Boolean. We noticed that the above first() method directly returns false, and that the emit in this method will throw an exception that the Flow has terminated. So when a value is received the Flow stops.

There are other methods of Flow, but if you want to learn more about them, you can check out Kotlin’s official documentation:

Kotlin. Making. IO/kotlinx cor…

Continue to optimize

Performing synchronous I/O operations on the UI thread can cause ANR or UI chaos, which can be alleviated by asynchronously preloading data from the DataStore:

override fun onCreate(savedInstanceState: Bundle?). {
    lifecycleScope.launch {
        dataStore.data.first()
        // You should also handle IOExceptions here.}}Copy the code

Toolclass encapsulation

I’ve wrapped the DataStore methods I used above into a utility class that you can use directly if you need to.

Below to see how think about how to write, first of all, this class should be a singleton, the whole project need to be used, of course, if you want to separate according to the business can also create multiple, if business simple point can be set directly into the singleton, set in the Kotlin singleton is very simple, direct use of the keyword object is ok, You can’t do that here, because the DataStore needs a context to initialize, so you need to pass in the context, so the singleton looks like this:

class DataStoreUtils private constructor(ctx: Context) {
    
    private var context: Context = ctx
  
    companion object {
        @Volatile
        private var instance: DataStoreUtils? = null

        fun getInstance(ctx: Context): DataStoreUtils {
            if (instance == null) {
                synchronized(DataStoreUtils::class) {
                    if (instance == null) {
                        instance = DataStoreUtils(ctx)
                    }
                }
            }
            returninstance!! }}}Copy the code

Then add the required global variables and initialize the DataStore:

private val preferenceName = "PlayAndroidDataStore"
private var dataStore: DataStore<Preferences>

init {
    dataStore = context.createDataStore(preferenceName)
}
Copy the code

Next, let’s add a few more methods to facilitate your daily use. Some people do not want to use different methods for each type. They prefer to use only one method. Then add several methods through generics.

Add the putData method first:

suspend fun <U> putData(key: String, value: U) {
    when (value) {
        is Long -> saveLongData(key, value)
        is String -> saveStringData(key, value)
        is Int -> saveIntData(key, value)
        is Boolean -> saveBooleanData(key, value)
        is Float -> saveFloatData(key, value)
        else -> throw IllegalArgumentException("This type can be saved into DataStore")}}Copy the code

This is also a suspend function. With a suspend function, let’s use another method that we don’t need in coroutines:

fun <U> putSyncData(key: String, value: U) {
    when (value) {
        is Long -> saveSyncLongData(key, value)
        is String -> saveSyncStringData(key, value)
        is Int -> saveSyncIntData(key, value)
        is Boolean -> saveSyncBooleanData(key, value)
        is Float -> saveSyncFloatData(key, value)
        else -> throw IllegalArgumentException("This type can be saved into DataStore")}}Copy the code

PutData is done, so let’s add the getData method:

fun <U> getData(key: String, default: U): Flow<U> {
    return when (default) {
        is Long -> readLongFlow(key, default) as Flow<U>
        is String -> readStringFlow(key, default) as Flow<U>
        is Int -> readIntFlow(key, default) as Flow<U>
        is Boolean -> readBooleanFlow(key, default) as Flow<U>
        is Float -> readFloatFlow(key, default) as Flow<U>
        else -> throw IllegalArgumentException("This type can be saved into DataStore")}}Copy the code

Also, add methods that are not used in coroutines:

fun <U> getSyncData(key: String, default: U): U {
    val res = when (default) {
        is Long -> readLongData(key, default)
        is String -> readStringData(key, default)
        is Int -> readIntData(key, default)
        is Boolean -> readBooleanData(key, default)
        is Float -> readFloatData(key, default)
        else -> throw IllegalArgumentException("This type can be saved into DataStore")}return res as U
}
Copy the code

At this point, the utility class is wrapped up, and whether you want to use it synchronously or asynchronously, the utility class will meet your needs.

If you want to save more trouble, I put this class on Github, you can use it directly, if it is helpful to you, you can click Star.

Github.com/zhujiang521…

Here is the test code address in this article:

Github.com/zhujiang521…

A delicate ending

I thought this essay was very simple and should be finished in a short time, but I kept writing for hours. Sometimes this is the case, like the first few articles I wrote about playing Android, each time I actually want to write a lot of things, but I don’t know how to write, but sometimes I feel that I can’t write a lot of things often can write a lot…

If your company’s projects are not particularly busy, consider migrating. Of course, if you want to wait for Google to release the official version, you can also use it.

If you are helpful, don’t forget to like and pay attention to it! Let me know in the comment section if there’s something wrong with this article. Thank you!

All right, I’ll see you guys later!!