Getting Started with Preference DataStore in Kotlin

February 21, 2021

DataStore is an improved data storage solution by Google to replace SharedPreferences for persisting simple pieces of data such as key-value pairs or typed objects with protocol buffers.

In this tutorial, we will learn how Jetpack DataStore works. We will work on how to change the UI mode of an app.

Introduction

Datastore uses Kotlin coroutines and flow to store data asynchronously, consistently, transactionally, and to handle data corruption. It works well with small simple datasets.

If you are working with large/complex datasets, consider using Room. This way you will not have to worry about referential integrity or partial updates.

Prerequisites

  • Creating projects in Android Studio.
  • Good understanding of Kotlin (as we will use it as our primary language).

Topics to be covered

  • What is Jetpack datastore.
  • Key differences between datastore and SharedPreferences.
  • How to implement datastore preferences.

Shifting from SharedPreferences

SharedPreferences is common among developers, but people are finding better solutions to store data that are more powerful and efficient. It has a few drawbacks that make it a little bit complex to work with.

If you have worked with Sharedpreferences you might have gotten an ANR (Application Not Responding) on your app. The most common reason is the long-running tasks on the main UI-thread.

This is because when using the app, you try to access the value of a particular key as soon as the app launches. With all that, you have to access Sharedpreferences to read the whole file no matter how large it is, bring the data in memory while all this is happening on the UI thread.

Why datastore?

  • It uses key-value pairs to store simple data.
  • Safe to call from the UI thread because the work is moved to Dispatchers.IO
  • It is safe from runtime exceptions.
  • Handles data migration.
  • Has transactional API with strong consistency guarantees.

Datastore provides two implementations

  1. Preference DataStore - stores key-value pairs. It is pretty similar to Sharedpreferences
  2. Proto DataStore - stores typed objects. This is by storing data as instances of a custom data type.

Less talk, show me the code

We will be adding Jetpack datastore to a project to change the UI mode, ie, from light to dark.

Step 1: Adding dependencies

Add the following dependency to your build.gradle in your app level.

// Preference DataStore
    implementation "androidx.datastore:datastore-preferences:1.0.0-alpha04"

Other important dependencies.

// Architectural Components
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"

// Coroutines
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.0'

    // Coroutine Lifecycle Scopes
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"

We will see how all these play a role in the app later.

Make sure that you are using the latest version for the stable release of each of these dependencies.

The app is a simple one. We will work on a pre-existing app by changing its UI mode.

Step 2: Creating a preference manager

Create a class called UIModePreference. This class holds the code that we will be using to read and write the data from the datastore.

package com.carolmusyoka.noteapp.data.datastore

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.preferencesKey
import androidx.datastore.preferences.createDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class UIModePreference(context: Context) {
 //1
    private val dataStore: DataStore<Preferences> = context.createDataStore(
        name = "ui_mode_preference"
    )
 //2
    suspend fun saveToDataStore(isNightMode: Boolean) {
        dataStore.edit { preferences ->
            preferences[UI_MODE_KEY] = isNightMode
        }
    }
 //3
    val uiMode: Flow<Boolean> = dataStore.data
        .map { preferences ->
            val uiMode = preferences[UI_MODE_KEY] ?: false
            uiMode
        }
 //4
    companion object {
        private val UI_MODE_KEY = preferencesKey<Boolean>("ui_mode")
    }

}

Code explanation

Creating the datastore

The 1st line basically creates a datastore using the file name "ui_mode_preference". The createDataStore() function is extension function created on Context.

Writing to the datastore

Preference DataStore provides the .edit() function to make it easier to update data. This method will save the UI mode from our activity.

Reading data from the datastore

DataStore exposes the stored data in a Flow in the preferences object which will emit values whenever the preferences are updated. It also ensures that the data is retrieved on Dispatcher.IO. we use map{} because we are mapping boolean values(remember we are storing boolean values in our datastore).

Storing preference using a key

We have created a key UI_MODE_KEY which will store the boolean value for either the light or dark mode. Preferences.preferencesKey() defines a key for each value that you need to store in the DataStore<Preferences>

Step 3: Creating ViewModel class

Create a new ViewModel class called UIViewModel.

class UIViewModel(application: Application):
        AndroidViewModel(application){

    
    private val uiDataStore = UIModePreference(application)

    // 1
    val getUIMode = uiDataStore.uiMode

    // 2
    fun saveToDataStore(isNightMode: Boolean) {
        viewModelScope.launch(Dispatchers.IO) {
            uiDataStore.saveToDataStore(isNightMode)
        }
    }
}

Reading from the datastore

The 1st line gets the UI mode from the datastore.

Writing from the datastore

Since the saveToDataStore() from the datastore preference class is a suspend function, it can only be called from a coroutine scope.

That is why we use viewModelScope.

Step 4: Working with the mainactivity

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.ui_menu, menu)

        // Set the item state
        lifecycleScope.launch {
            val isChecked = viewModel.getUIMode.first()
            val item = menu.findItem(R.id.action_night_mode)
            item.isChecked = isChecked
            setUIMode(item, isChecked)
        }
}
    

 
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // Handle action bar item clicks here.
        return when (item.itemId) {
            R.id.action_night_mode -> {
                item.isChecked = !item.isChecked
                setUIMode(item, item.isChecked)
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }

    private fun setUIMode(item: MenuItem, isChecked: Boolean) {
        if (isChecked) {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
            viewModel.saveToDataStore(true)
            item.setIcon(R.drawable.ic_night)

        } else {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
            viewModel.saveToDataStore(false)
            item.setIcon(R.drawable.ic_day)

        }
    }


Updating preferences

Using setUIMode(), we are updating the state of the preference if the icon is checked. We are updating the image icon and the background colour so that the UI is changed.

Once you are done, run the app.

Here is what you would expect. light mode

dark mode

You can check out the entire project on GitHub.

Conclusion

The Google team is trying to make Android development a little bit easier each day by rolling out new and improved libraries. They may change or deprecate from time to time and learning about these tools can put us ahead of the curve.

So keep learning.

Happy coding!!

Resources


Peer Review Contributions by: Odhiambo Paul


About the author

Carol Musyoka

Carol is an undergraduate student, pursuing a degree in Mathematics and Computer Science. She is an Android Developer with a speciality in Kotlin. She is also a GitHub Campus Expert. Carol loves growing the developer community in her school and Kenya generally. She loves reading African literature, listening to music, gaming, travelling and calligraphy.

This article was contributed by a student member of Section's Engineering Education Program. Please report any errors or innaccuracies to enged@section.io.