Florina Muntenescu, Android Development technical Extension Engineer and Rohit Sathyanarayana, Google Software Engineer

Welcome to Jetpack DataStore, a new and improved data storage solution designed to replace the old SharedPreferences. Jetpack DataStore is developed based on Kotlin coroutines and flows and provides two different implementations: Proto DataStore and Preferences DataStore. Proto DataStore, which can store objects with types (using protocol buffers); Preferences DataStore, which can store key-value pairs. In DataStore, data is stored in an asynchronous, consistent, transactional manner, overcoming most of the shortcomings of SharedPreferences.

SharedPreferences and DataStore comparison

  • SharedPreferences has a synchronization API that looks like it can be safely called from the UI thread, but that API actually performs disk I/O operations. In addition, the apply() method blocks the UI thread at fsync(). Waiting for fsync() is triggered whenever a Service or Activity starts or stops anywhere in your application. Fsync () calls scheduled by apply() block the UI thread, which is also a common source of ANR.

  • SharedPreferences throws a runtime exception when parsing errors occur.

In both implementations, unless otherwise specified, DataStore stores the preferences in a file and all data operations are performed on Dispatchers.io.

Although both the Preferences DataStore and the Proto DataStore can store data, they implement it differently:

  • Preference DataStore, like SharedPreferences, cannot define a schema or ensure that key values are accessed with the correct type.
  • Proto DataStore lets you define schemas using Protocol Buffers. With Protobufs, strongly typed data can be retained. They are faster, smaller, and less ambiguous than XML or other similar data formats. Although Proto DataStore requires you to learn a new serialization mechanism, we think it’s worth the trade-off given the advantages of strongly typed Schemas that Proto DataStore brings.

Comparison between Room and DataStore

If you have a need to locally update data, referential integrity, or support large, complex data sets, you should consider using Room instead of DataStore. DataStore is ideal for small, simple data sets that do not support local updates or referential integrity.

Use the DataStore

Start by adding DataStore dependencies. If you are using Proto DataStore, make sure you also add Proto dependencies:

Def dataStoreVersion = "1.0.0-alpha05" // Check the latest version number on the Android developer website // https://developer.android.google.cn/jetpack/androidx/releases/datastore // Preferences DataStore implementation "androidx.datastore:datastore-preferences:$dataStoreVersion" // Proto DataStore implementation "androidx.datastore:datastore-core:$dataStoreVersion"Copy the code

When you use Proto DataStore, you need to define your own schema using Proto files in the app/ SRC /main/ Proto/directory. For more information on defining Proto Schema, see the Protobuf language guide.

syntax = "proto3";

option java_package = "<your package name here>";
option java_multiple_files = true;

message Settings {
  int my_counter = 1;
}
Copy the code

Create the DataStore

You can create datastores using the context.createdatastore () extension method:

// Create the Preferences DataStore
val dataStore: DataStore<Preferences> = context.createDataStore(
    name = "settings"
)
Copy the code

If you are using Proto DataStore, you will also need to implement the Serializer interface to tell the DataStore how to read and write your data types.

object SettingsSerializer : Serializer<Settings> {
    override fun readFrom(input: InputStream): Settings {
        try {
            return Settings.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
}

// Create Proto DataStore
val settingsDataStore: DataStore<Settings> = context.createDataStore(
    fileName = "settings.pb",
    serializer = SettingsSerializer
)
Copy the code

Read data from the DataStore

The DataStore exposes stored data as a Flow, whether it is a Preferences object or an object you define in Proto Schema. DataStore ensures that data is retrieved on Dispatchers.io, so it doesn’t block your UI thread.

Using the Preferences DataStore:

val MY_COUNTER = preferencesKey<Int> ("my_counter")
val myCounterFlow: Flow<Int> = dataStore.data
     .map { currentPreferences ->
        // Unlike Proto DataStore, type safety is not guaranteed.currentPreferences[MY_COUNTER] ? :0   
   }
Copy the code

Using Proto DataStore:

val myCounterFlow: Flow<Int> = settingsDataStore.data
    .map { settings ->
        // The myCounter attribute is generated by your Proto Schema!
        settings.myCounter 
    }
Copy the code

Writes data to the DataStore

To write data, DataStore provides a datastore.updateData () suspend function that gives you the state of the currently stored data as a parameter, either for the Preferences object or for the object instance that you define in Proto Schema. The updateData() function uses atomic read, write, and modify operations to updateData in a transactional manner. This coroutine completes when the data has been stored on disk.

The Preferences DataStore also provides a datastore.edit () function to facilitate data updates. In this function, you receive a MutablePreferences object for editing, instead of a Preferences object. This function, like updateData(), applies the changes to disk after the conversion block is complete, and the coroutine completes when the data is stored on disk.

Using the Preferences DataStore:

suspend fun incrementCounter(a) {
    dataStore.edit { settings ->
        // We can safely increase our counters without losing data due to resource contention.
        valcurrentCounterValue = settings[MY_COUNTER] ? :0
        settings[MY_COUNTER] = currentCounterValue + 1}}Copy the code

Using Proto DataStore:

suspend fun incrementCounter(a) {
    settingsDataStore.updateData { currentSettings ->
        // We can safely increase our counters without losing data due to resource contention.
        currentSettings.toBuilder()
            .setMyCounter(currentSettings.myCounter + 1)
            .build()
    }
}
Copy the code

Migrate from SharedPreferences to DataStore

Migrated to a DataStore from the SharedPreferences, you need to pass SharedPreferencesMigration object DataStore constructor, DataStore can automatically migrate from SharedPreferences to DataStore. The migration is run before any data access occurs in the DataStore, which means that your migration must have been successful before datastore.data returns any values and datastore.updateData () can update the data.

If you want to migrate to the Preferences DataStore, you can use SharedPreferencesMigration default implementation. Just pass in the name you used when you constructed the SharedPreferences.

Using Preferences DataStore:

val dataStore: DataStore<Preferences> = context.createDataStore(
    name = "settings",
    migrations = listOf(SharedPreferencesMigration(context, "settings_preferences")))Copy the code

When migrating to the Proto DataStore, you must implement a mapping function that defines how to migrate the key-value pairs used by SharedPreferences to the DataStore Schema that you define.

Using Proto DataStore:

val settingsDataStore: DataStore<Settings> = context.createDataStore(
    produceFile = { File(context.filesDir, "settings.preferences_pb") },
    serializer = SettingsSerializer,
    migrations = listOf(
        SharedPreferencesMigration(
            context,
            "settings_preferences"            
        ) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences ->
            // Map sharedPrefs to your type here.}))Copy the code

conclusion

SharedPreferences has many flaws: the synchronization API that appears to be safe to call from the UI thread is not secure, there is no mechanism for error notification, there is no transaction API, and so on. DataStore is an alternative to SharedPreferences that solves most of the problems with SharedPreferences. DataStore includes fully asynchronous apis implemented using Kotlin coroutines and Flow to handle data migration, data consistency, and data corruption.

Since DataStore is still in beta, we need your help to make it better! First, you can learn more about DataStore through our documentation and also through the two Codelabs we’ve prepared for you: Preferences DataStore codelab and Proto DataStore codelab to try DataStore. Finally, you can create problems on the problem tracker to let us know how to improve the DataStore.