Long time no see

Hi everyone, long time no see, last time I posted an article was March, and soon it will be June. Recently I have been too busy to write, and I haven’t posted an article for two or three months. I happened to have some time on the weekend, so I want to write something.

A piece of red

In a previous DataStore post: Give Jetpack DataStore a big hug, the version used at the time was 1.0.0-Alpha05, which is now in beta after more than six months of updates from the first alpha release in September 2020.

As you all know, Google’s alpha library API could be changed at any time, and my guess turned out to be correct, as the DataStore version became a hit.

But don’t worry, the release of the beta proves that the API is basically complete, which means that there will be no major changes to the API after that, just performance optimizations.

Begin to use

I also wrote a utility class to use DataStore, but that seems redundant now. Let’s see how to use the beta version of DataStore.

Add the dependent

The first step is definitely to add a dependency.

// DataStore
implementation "Androidx. Datastore: datastore - preferences: 1.0.0 - beta01"
Copy the code

Initialize the DataStore

In previous alpha releases, the initialization method was as follows:

context.createDataStore(preferenceName)
Copy the code

Create a DataStore using createDataStore, an extension of the Context. This method has been removed. How do you createDataStore? You should now create the Datastore

instance using the property delegate created by the preferencesDataStore.

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "PlayAndroidDataStore")
Copy the code

By calling the preferencesDataStore method just once at the top level in your project, you can access the instance through this property throughout the rest of your application, making it easier to keep the DataStore as a singleton.

Let’s take a look at the source code for preferencesDataStore.

public fun preferencesDataStore(
    name: String,
    corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
    produceMigrations: (Context) - >List<DataMigration<Preferences>> = { listOf() },
    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): ReadOnlyProperty<Context, DataStore<Preferences>> {
    return PreferenceDataStoreSingletonDelegate(name, corruptionHandler, produceMigrations, scope)
}
Copy the code

PreferencesDataStore has four parameters, of which only the name is mandatory. The rest of the parameters have default values.

  • name:DataStoreName, not much to say;
  • CorruptionHandler: If the DataStore encounters a CorruptionException while trying to read data, it will call destroyHandler. When data cannot be deserialized, the serializer raises a CorruptionException;
  • ProduceMigrations: Generates migration. ApplicationContext is passed as a parameter to these callbacks. DataMigrations are run before any access to the data. Each producer and migration can run more than once, regardless of success (perhaps because another migration failed or a write to disk failed).
  • Scope: scope

This section describes the parameters of the preferencesDataStore method. You can fill in the parameters as required.

Use the DataStore

Create the DataStore

To do this, we first need to call the extended property of the Context we defined: dataStore.

private lateinit var dataStore: DataStore<Preferences>

/**
 * init Context
 * @param context Context
 */
fun init(context: Context) {
    dataStore = context.dataStore
}
Copy the code

You can then operate from the dataStore.

Save the data

Let’s take a look at how to save data.

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

suspend fun saveIntData(key: String, value: Int) {
    dataStore.edit { mutablePreferences ->
        mutablePreferences[intPreferencesKey(key)] = value
    }
}

suspend fun saveStringData(key: String, value: String) {
    dataStore.edit { mutablePreferences ->
        mutablePreferences[stringPreferencesKey(key)] = value
    }
}

suspend fun saveFloatData(key: String, value: Float) {
    dataStore.edit { mutablePreferences ->
        mutablePreferences[floatPreferencesKey(key)] = value
    }
}

suspend fun saveLongData(key: String, value: Long) {
    dataStore.edit { mutablePreferences ->
        mutablePreferences[longPreferencesKey(key)] = value
    }
}
Copy the code

When you use it, you can save it separately according to different types. Of course, if you don’t want to divide it into so much detail, you can also write a method to call it uniformly.

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

Read the data

After saving the data, it’s time to read. Let’s see how to read the data.

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[booleanPreferencesKey(key)] ? : default }fun readIntFlow(key: String, default: Int = 0): Flow<Int> =
    dataStore.data
        .catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throwit } }.map { it[intPreferencesKey(key)] ? : default }fun readStringFlow(key: String, default: String = ""): Flow<String> =
    dataStore.data
        .catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throwit } }.map { it[stringPreferencesKey(key)] ? : default }fun readFloatFlow(key: String, default: Float = 0f): Flow<Float> =
    dataStore.data
        .catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throwit } }.map { it[floatPreferencesKey(key)] ? : default }fun readLongFlow(key: String, default: Long = 0L): Flow<Long> =
    dataStore.data
        .catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throwit } }.map { it[longPreferencesKey(key)] ? : default }Copy the code

Similarly, select a different method to use according to the type, of course, you can also write a method like the above to save the data for unified call.

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

Use the advanced

We’ve described how to use DataStore simply, but only with basic types. What if we wanted to use data classes? Of course you can use the Proto supported by DataStore, but we won’t talk about that here, because DataStore can also be used in Kotlin directly through the serialization of data classes. Let’s see how it works.

Defining data classes

First let’s define a data class.

data class ZhuPreferences(
    val name: String,
    val age: Int
)
Copy the code

The data class is simple, with only two parameters, name and age.

Implement DataStore serializer

The next step is to implement the serializer for the DataStore.

@Serializable
data class ZhuPreferences(
    val name: String = "jiang".val age: Int = 20
)

object ZhuPreferencesSerializer : Serializer<ZhuPreferences> {
    override val defaultValue = ZhuPreferences()

    override suspend fun readFrom(input: InputStream): ZhuPreferences {
        try {
            return Json.decodeFromString(
                ZhuPreferences.serializer(), input.readBytes().decodeToString()
            )
        } catch (serialization: SerializationException) {
            throw CorruptionException("Unable to read UserPrefs", serialization)
        }
    }

    override suspend fun writeTo(t: ZhuPreferences, output: OutputStream) {
        output.write(Json.encodeToString(ZhuPreferences.serializer(), t).encodeToByteArray())
    }
}
Copy the code

Since Parcelables are not safe to use with DataStore because the data format may change between Android versions, Serializable is used here.

Use the DataStore serializer

Create the DataStore

Now that the data class is ready, let’s create the DataStore. Instead of using the preferencesDataStore method mentioned above, we need to create the DataStore method.

val Context.dataStores by dataStore("test", serializer = ZhuPreferencesSerializer)
Copy the code

Write data

Let’s see how to write data to a data class.

private suspend fun setDataStore(zhuPreferences: ZhuPreferences) {
    dataStores.updateData {
        it.copy(name = zhuPreferences.name, age = zhuPreferences.age)
    }
}
Copy the code

As you can see from the code above, you modify the data using the updateData method in dataStores and then update the data using the generated.copy() function.

Read the data

Let’s see how to read the data once the write is complete.

private suspend fun getDataStore(a) {
    val name = dataStores.data.first().name
    val age = dataStores.data.first().age
}
Copy the code

Since the serializer is already passed in when dataStores are created, it is easy to read the data and simply call up the parameters in the data class for use.

conclusion

The Flow and coroutine versions of the DataStore are available in beta, but the performance of the DataStore is still poor. However, I believe DataStore will match or surpass MMKV and SP in performance by the time the official version is released.

Please don’t forget to like, follow, and bookmark if you have any questions, let me know in the comments section.