DataStore Tutorial For Android: Getting Started

In this tutorial you’ll learn how to read and write data to Jetpack DataStore, a modern persistance solution from Google. By Luka Kordić.

4.2 (9) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

Migrating SharedPreferences to Preferences DataStore

First, open the app-level build.gradle file and verify you have the following dependency:

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

This dependency lets you use the Prefs DataStore API.

Now you’ll create an abstraction for Prefs DataStore.

Creating an Abstraction for Prefs DataStore

In the project pane on the left, navigate to learningcompanion. Then create a new prefsstore package. In the newly created package, create an Kotlin interface called PrefsStore and a Kotlin class called PrefsStoreImpl.

You’ll see a structure like this:

Prefsstore package with PrefsStore interface and PrefsStoreImpl class

Now open PrefsStore.kt and add:

fun isNightMode(): Flow<Boolean>

suspend fun toggleNightMode()

To import Flow, select the import from the kotlinx.coroutines.flow package.

You created an interface as an abstraction layer for all of your interactions with the Preferences DataStore. Then you added functons to the interface to represent your DataStore operations. It contains isNightMode() that returns a Flow. The Flow will represent the app setting that tells you if the night mode is on or off.

You also create toggleNightMode() with the suspend modifier to change the night mode option when the user taps on the theme icon. You added a suspend modifier because the function will contain another suspend function later.

It’s time to write the interface implementation!

Creating Prefs DataStore

Open PrefsStoreImpl.kt and update the class name with a constructor and @Inject:

class PrefsStoreImpl @Inject constructor(
  @ApplicationContext context: Context) : PrefsStore {
}

Here you provide the context you’ll use to create a DataStore. @Inject lets you use Context here. @ApplicationContext and @Inject are annotations from Hilt, which let you provide the app-level context to your components.

Now, add a constant above the class name:

private const val STORE_NAME = "learning_data_store"

You’ll use this constant as the DataStore name in the next step.

Then, add the following code inside the class:

private val dataStore = context.createDataStore(
  name = STORE_NAME,
  migrations = listOf(SharedPreferencesMigration(context, PREFS_NAME))
)

Here, you add code for creating an instance of DataStore using createDataStore(). Then you pass in the constant value for name and a list of migrations you want to run upon creation. You use a built-in SharedPreferencesMigration() to create a migration. This will move all the data from your SharedPreferences to the DataStore.

To add the missing import, press Option-Return on macOS or Alt-Enter on Windows. Import createDataStore() without the Serializer parameter and SharedPreferencesMigration from androidx.datastore.preferences.

Note: You’ll get an error at this step because you didn’t override all of the methods from the interface. Don’t worry: You’ll fix this in the next step.

That’s it! It’s that easy to migrate your data from SharedPreferences to DataStore. :]

Now that you have your DataStore, it’s time to learn how to read data from it.

Reading Data From Prefs DataStore

In PrefsStoreImpl.kt, at the end of the file, add:

private object PreferencesKeys {
  val NIGHT_MODE_KEY = preferencesKey<Boolean>("dark_theme_enabled")
}

This piece of code creates a Kotlin object which holds the keys you’ll use to read and write data.

Below your dataStore value, add:

override fun isNightMode() = dataStore.data.catch { exception -> // 1
  // dataStore.data throws an IOException if it can't read the data
  if (exception is IOException) { // 2
    emit(emptyPreferences())
  } else {
    throw exception
  }
}.map { it[PreferencesKeys.NIGHT_MODE_KEY] ?: false } // 3

Here’s a code breakdown:

If the key isn’t set when you try to read the data it returns null. You use the Elvis operator to handle this and return false instead.

  1. On the first line, you access the data of DataStore. This property returns a Flow. Then you call catch() from the Flow API to handle any errors.
  2. In the lambda block, you check if the exception is an instance of IOException. If it is, you catch the exception and return an empty instance of Preferences. If the exception isn’t IOException, you rethrow it or handle it in a way that works for you.
  3. Finally, map() returns a Flow which contains the results of applying the given function to each value of the original Flow. In your case, you get the data by using a certain key, the PreferencesKeys.NIGHT_MODE_KEY.

Now implement toggleNightMode() from the interface to avoid errors. Add isNightMode() below:

override suspend fun toggleNightMode() {
}

This code is only a declaration of the method you’ll implement later. So leave the body empty for now.

Next, you’ll observe values from Prefs DataStore.

Observing Values From Prefs DataStore

In the previous step, you wrote the code to read the value from the store, but you didn’t use it. Now, you’ll collect the flow in your ViewModel.

Open CoursesViewModel.kt. Then remove sharedPrefs from the constructor and replace it with:

private val prefsStore: PrefsStore

With this code, Hilt library injects the instance for you.

Hooray! You’re not using Shared Preferences anymore so remove everything related to it. Delete:

  • init
  • Everything inside viewModelScope.launch {} in toggleNightMode()
  • Both darkThemeEnabled and _darkThemeEnabled values

You need to make one more change before Hilt can inject the instance.

Open StoreModule.kt. Uncomment @Binds and bindPrefsStore(). While outside of the scope for this tutorial, this code told Hilt how to provide the instances.

Now that you have access to the store, go back to CoursesViewModel.kt. Below the class declaration add:

val darkThemeEnabled = prefsStore.isNightMode().asLiveData()

asLiveData() lets you convert Flow to LiveData.

Note: Flow won’t emit any values until you subscribe to LiveData. You can also call collect() to get the data directly from Flow without converting it to LiveData.

Build and run. The app should look like before, but now it’s reading from the DataStore instead of SharedPreferences.

Application main screen in the dark mode

This also proves your migration worked! Now it’s time to write the data to DataStore and toggle the night mode for the app.

Writing Data to DataStore

You finally prepared the code to store the current theme value into the DataStore.

Now, open PrefsStoreImpl.kt. Find toggleNightMode() and add the following code:

dataStore.edit {
  it[PreferencesKeys.NIGHT_MODE_KEY] = !(it[PreferencesKeys.NIGHT_MODE_KEY] ?: false)
}

Here’s a code breakdown:

  1. To write data to the Prefs DataStore you call edit(), which is a suspend and extension function on DataStore.
  2. It saves data transactionally in an atomic, read-modify-write operation. Atomic means you don’t have to worry about threading, as all operations are safe and there are no race conditions.
  3. When called, this function suspends the current coroutine until the data persists to disk.
  4. When this process finishes, DataStore.data reflects the change and you get a notification about the changes you subscribed to.
  5. To change the value, you obtain the current value by using PreferencesKeys.NIGHT_MODE_KEY. You invert and store it again.

To complete this step, open CoursesViewModel.kt. Then locate toggleNightMode() and add the following code to launch():

prefsStore.toggleNightMode()

Here you call the method you implemented to toggle the current theme.

Build and run. Change the theme and then restart the app. You’ll notice the theme persists.

Application main screen in the dark mode

Congratulations! You successfully implemented the theme change functionality by reading and writing to Prefs DataStore.

Next, you’ll take a look at Proto DataStore, to store more complex types of data.