DataStore Preferences and migrating from SharedPreferences

Anyone that used the SharedPreferences in the "modern" Android development era (I define this after the launch of the Jetpack project) knew that the API desperately needed an update: only a synchronous way was there to read data, no mechanism to indicate errors, and no way of doing transactional commits.

Most of the time, I would end up abstracting SharedPreferences usage just to add an async layer and handle possible errors. Although this was not a huge task, I would keep repeating it because it was too small to worth extracting it to a common library.

Jetpack recently introduced DataStore which aims to solve exactly this problem: a convenient modern way of storing small bits of data. This kind of replaces the SharedPreferences (and the official advice is to avoid using it).

The DataStore library comes in 2 parts: the DataStore Preferences and Proto DataStore. We will only deal with the former that does not offer any type safety on the stored values and it's easier to migrate if you are already using SharedPreferences. The latter seems super interesting and offers type safety  (check out this post).

Proto DataStore (and Protocol Buffers intro)
Jetpack Proto DataStore is the new way of storing small data aiming at replacing the usage of the aging SharedPreferences API using Protocol Buffers.

Set up

Modify your build.gradle to add the library dependency. Before copy-pasting, check for the latest version.

implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"

Reading

Firstly, create a DataStore instance. The only required parameter is the unique  name. Preferences is a class provided by the library to implement key-value storage (like SharedPreferences). Finally, createDataStore() is an extension method on Context provided by the library.

private val dataStore: DataStore<Preferences> =
        context.createDataStore(name = "movies")

Create a preferenceKey for each key you want to use. Note that the value type of these keys can only be one of Int, Long, Boolean, Float, String. There's a preferencesSetKey to use for a Set value as well.

val LAST_MOVIE_ID = preferencesKey<Int>("last_movie_id")

Read by getting a Flow of the values for the requested key. An IOException might be thrown while reading the data, so you might choose to handle this case gracefully instead of throwing. Don't forget to handle what happens when a value does not exist yet (i.e. when null is returned).

val lastMovieIdFlow: Flow<Int> = dataStore.data
    .catch { exception ->
        if (exception is IOException) {
            emit(-1)
        } else {
            throw exception
        }
    .map { preferences ->
        preferences[LAST_MOVIE_ID] ?: -1
    }

To use this value in your ViewModel you can call lastMovieIdFlow.asLiveData() or lastMovieIdFlow.collect(). Read this for a quick intro into Flow.

Writing

Write using the edit() suspend function that completes when the writing is done. Again, an IOException might be thrown if something goes wrong with reading or writing.

dataStore.edit { preferences ->
    preferences[LAST_MOVIE_ID] = currentMovieId
}

Migrating

Finally, if you already have an Android app, probably you are already using SharedPreferences. To migrate to DataStore, when creating the `DataStore instance, just pass the old SharedPreferences name and the migration will be performed automatically. After the migration is successful (i.e. no exception is thrown) you can stop using SharedPrefernces and start reading/writing your key-value pairs using DataStore.

private val dataStore: DataStore<Preferences> =
    context.createDataStore(
        name = "movies",
        migrations = 
          listOf(SharedPreferencesMigration(context, "shared_preferences_name"))
    )

Hopefully, this was a useful super quick intro to the usage of DataStore Preferences. For more details check the excellent documentation, code lab or official blog post.