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).
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.