Using the Preference DataStore in a more generic/scalable way.
Android Jetpack to the Space
Hello, Welcome to my article. I want to share my two cents on Preference Datastore and on how to use it in a generic method.
What is Datastore?
Let’s first understand Datastore.
So earlier, we had two options to store data in Android:
- Shared Preference.
- Room (Built on top of SQLite)
Now Shared Preference was used to store a small amount of data, that would be in Key-value pair format, while Room was used to store a large amount of data in a structured format.
In comes Datastore, a new way of storing a small amount of data, which is built to store data asynchronously, consistently, and transactionally. Datastore is built on top of Kotlin Coroutines and Flow which is a huge supporter of asynchronous programming.
There are two types of DataStore:-
- Preference DataStore ->
* Used to store Data in Key Value format (Just like in Shared Preference).
* Doesn’t need a pre-defined schema like SQL or Proto-DataStore.
* Doesn’t provide type-safety. - Proto DataStore ->
* Used to store data in custom data format (Complex/Custom Datatypes).
* Need to define schema using Protobuffers.
* Provides type-safety.
Implementing Preference DataStore
To implement Preference Datastore in our project we need to add a few dependencies in our build. gradle file of our app:
// For preference Datastore implementation "androidx.datastore:datastore-preferences:1.0.0"
Some additional dependencies that one might need will be:
//For Coroutines if not added earlier implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' //For lifecycle if not added earlier implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha02" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha02"
Make sure you are always using the latest stable dependencies for a better experience.
Code Implementation
This will be our file structure. Naming it “Local”, to store it locally in our device.
We will make our Preference Datastore File in the Data file of our project. It will consist of three major files, an interface, an object, and a helper class. Let’s see the implementation one by one.
Interface IPreferenceDataStoreAPI
package com.example.datastoretest.data.local | |
import androidx.datastore.preferences.core.Preferences | |
import kotlinx.coroutines.flow.Flow | |
interface IPreferenceDataStoreAPI { | |
suspend fun <T> getPreference(key: Preferences.Key<T>,defaultValue: T):Flow<T> | |
suspend fun <T> getFirstPreference(key: Preferences.Key<T>,defaultValue: T):T | |
suspend fun <T> putPreference(key: Preferences.Key<T>,value:T) | |
suspend fun <T> removePreference(key: Preferences.Key<T>) | |
suspend fun <T> clearAllPreference() | |
} |
We will create a new Interface Class which will contain all our suspend functions. The role of this interface is to allow us to implement our functions in a more structured way. It will contain functions to GET, PUT, REMOVE and REMOVE ALL. CRUD operations can be carried out. But you can make your functions based on your requirement.
Now we have two GET functions declared here.
suspend fun <T> getPreference(key: Preferences.Key<T>,defaultValue: T):Flow<T> suspend fun <T> getFirstPreference(key: Preferences.Key<T>,defaultValue: T):T
Both of these have the same role, i.e., to GET the data from Datastore. But the first function’s return type is Flow which will return a Flow/Stream of data. While the Second one returns just the value of Data.
If we talk about Flow a little, it means a Stream of Data. Now if the value changes dynamically, the same gets reflected on UI. Now we don’t want it to happen everywhere, especially when we are displaying a name to a User or checking some value. For that, we will use the Second function which will return just T.
Object PreferenceDataStoreConstants
This is an Object class whose job is to hold the name of our variables along with the datatype. But here is a twist. We don’t write it as usual:
val name: String = "Android Dev"
Rather we write it as:
val STRING_KEY = stringPreferencesKey("STRING_KEY")
We concatenate the datatype in all small ahead of “PreferenceKey()”, like longPreferencesKey(“name_of_variable”), booleanPreferencesKey(“name_of_variable”), etc.
package com.example.datastoretest.data.local | |
import androidx.datastore.preferences.core.booleanPreferencesKey | |
import androidx.datastore.preferences.core.intPreferencesKey | |
import androidx.datastore.preferences.core.longPreferencesKey | |
import androidx.datastore.preferences.core.stringPreferencesKey | |
object PreferenceDataStoreConstants { | |
val IS_MINOR_KEY = booleanPreferencesKey("IS_MINOR_KEY") | |
val AGE_KEY = intPreferencesKey("AGE_KEY") | |
val NAME_KEY = stringPreferencesKey("NAME_KEY") | |
val MOBILE_NUMBER = longPreferencesKey("MOBILE_NUMBER") | |
} |
Job Offers
Class PreferenceDataStoreHelper
Let’s get to the business end of implementing our DataStore Helper class which will store and retrieve data for us.
The first thing we need to do is to create our DataStore:
private val Context.dataStore by preferencesDataStore( name = "PreferenceDataStore" )
This line needs to be written outside of HelperClass.
Let’s now define our functions and see their implementation:
package com.example.datastoretest.data.local | |
import android.content.Context | |
import androidx.datastore.preferences.core.Preferences | |
import androidx.datastore.preferences.core.edit | |
import androidx.datastore.preferences.core.emptyPreferences | |
import androidx.datastore.preferences.preferencesDataStore | |
import kotlinx.coroutines.flow.Flow | |
import kotlinx.coroutines.flow.catch | |
import kotlinx.coroutines.flow.first | |
import kotlinx.coroutines.flow.map | |
import java.io.IOException | |
// Declaring/Creating the DataStore File for Application | |
private val Context.dataStore by preferencesDataStore( | |
name = "PreferenceDataStore" | |
) | |
class PreferenceDataStoreHelper(context: Context):IPreferenceDataStoreAPI { | |
// dataSource access the DataStore file and does the manipulation based on our requirements. | |
private val dataSource = context.dataStore | |
/* This returns us a flow of data from DataStore. | |
Basically as soon we update the value in Datastore, | |
the values returned by it also changes. */ | |
override suspend fun <T> getPreference(key: Preferences.Key<T>, defaultValue: T): | |
Flow<T> = dataSource.data.catch { exception -> | |
if (exception is IOException){ | |
emit(emptyPreferences()) | |
}else{ | |
throw exception | |
} | |
}.map { preferences-> | |
val result = preferences[key]?: defaultValue | |
result | |
} | |
/* This returns the last saved value of the key. If we change the value, | |
it wont effect the values produced by this function */ | |
override suspend fun <T> getFirstPreference(key: Preferences.Key<T>, defaultValue: T) : | |
T = dataSource.data.first()[key] ?: defaultValue | |
// This Sets the value based on the value passed in value parameter. | |
override suspend fun <T> putPreference(key: Preferences.Key<T>, value: T) { | |
dataSource.edit { preferences -> | |
preferences[key] = value | |
} | |
} | |
// This Function removes the Key Value pair from the datastore, hereby removing it completely. | |
override suspend fun <T> removePreference(key: Preferences.Key<T>) { | |
dataSource.edit { preferences -> | |
preferences.remove(key) | |
} | |
} | |
// This function clears the entire Preference Datastore. | |
override suspend fun <T> clearAllPreference() { | |
dataSource.edit { preferences -> | |
preferences.clear() | |
} | |
} | |
} |
This is basically how we can use Preference Datastore in a way that allows us to be more generic and adding/removing new keys won’t be a hectic task compared to the old and defined way.
Implementing it to Store/Retrieve Data.
Now that, we have implemented the entire DataStore along with its helper class, let’s see how to store and retrieve the data.
Since these functions are asynchronous we have to use them inside the Coroutines Scope. In general, all the functions related to DataStore must be asynchronous.
We have two options to call these functions either from ViewModel (the recommended way) or from Activity (the not so recommended way).
For ViewModel, we use viewModelScope.launch{…} function like this:
// Retriving Using the Flow Method | |
viewModelScope.launch { | |
preferenceDataStoreHelper.getPreference(NAME_KEY,"").collect { | |
name = it | |
} | |
} | |
// Retriving Using the Not Flow / static data Method | |
viewModelScope.launch { | |
val name = preferenceDataStoreHelper.getFirstPreference(NAME_KEY,"") | |
} | |
// Setting Data | |
val name = "Android Developer" | |
viewModelScope.launch { | |
preferenceDataStoreHelper.putPreference(NAME_KEY,name) | |
} |
ViewModel Implementation Demo
For Activity/Fragment, we use lifecycleScope.launch{…} function like this:
// Retriving Using the Flow Method | |
lifecycleScope.launch { | |
preferenceDataStoreHelper.getPreference(NAME_KEY,"").collect { | |
name = it | |
} | |
} | |
// Retriving Using the Not Flow / static data Method | |
lifecycleScope.launch { | |
val name = preferenceDataStoreHelper.getFirstPreference(NAME_KEY,"") | |
} | |
// Setting Data | |
val name = "Android Developer" | |
lifecycleScope.launch { | |
preferenceDataStoreHelper.putPreference(NAME_KEY,name) | |
} |
Activity Implementation Demo
That’s a wrap, folks !
Maybe Next up we might see how to store data in Preference Datastore in an encrypted way. Till then, thanks all, and happy coding!
This article was originally published on proandroiddev.com on December 29, 2022