Getting Started with Preference DataStore in Kotlin
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. <!--more--> 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
- Preference DataStore - stores key-value pairs. It is pretty similar to Sharedpreferences
- 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.
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