Jetpack DataStore Implementation for Android

Jetpack DataStore is an improved data storage solution that utilizes Kotlin coroutines and flows to store key-value pairs asynchronously, transitionally, and safely.
DataStore offers us two implementations:

  • Preferences DataStore -stores and retrieves key-value pair data using keys. It is important to note that this implementation does not provide type safety.
  • Proto DataStore -stores typed objects of custom data types. Proto DataStore implementation requires one to define custom schema using protocol buffers; for more information about protocol buffers, check out the documentation here.


You may probably be wondering why you should consider using DataStore over the usual SharedPreferences? Let's have a look at some of the reasons why: Jetpack DataStore provides:

  • Thread safety - operations are moved to Dispatchers. IO automatically, thus you can store and/or retrieve data without blocking the main thread.
  • Type safety
  • Fully asynchronous API
  • Error handling mechanism.

For more comparison of the two, look at the official documentation.
In this article, we will learn Preferences DataStore then, in a later article, we will learn the implementation of Proto DataStore.

Getting started
I will guide you on implementing Preferences Datastore by referencing a project I am working on where I am making a POST network request to sign in endpoint; the server then returns a response containing an authorization token and user object.
The project uses MVVM architecture and Koin for Dependency Injection.
We will save our received token to Preferences DataStore and retrieve it later to make other requests requiring an Authorization token.
First, open your app-level build.gradle file and add the following dependency.

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

Creating Prefs DataStore abstraction
Create a new package in your project’s root package and call it anything you like; in my case, I called it prefsstore. Then, in the newly created package, create two files an interface again; the name can be anything, for example, PreferencesStore and a class PrefrencesStoreImpl that implements the interface.

interface PreferencesStore {}
class PreferencesStoreImpl(context: Context): PreferencesStore {}

Open PreferencesStoreImpl and add the following, this code creates a property of Context receiver type and delegates its value to preferencesDataStore()

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(STORE_NAME)

At the top of the file, add the following constant.

private const val STORE_NAME = "my_app_store"

In our PreferencesStore interface, add the following two methods saveToken() to save the token received and getToken() to retrieve it.

suspend fun saveToken(token: String)
fun getToken(): Flow<String>

Modify PreferencesStoreImpl class to conform to PreferencesStore interface by implementing the two methods. To save data in our case token, we will need to define a key that we will use to retrieve the data later.
Add the following code snippet at the end of PreferencesStoreImpl class.

private object PreferencesKeys {
    val TOKEN_KEY = stringPreferencesKey("auth_token")
}

Writing Data to Preferences DataStore
Preferences DataStore provides a suspend edit() extension function that writes data transactionally in a read-write operation. In our saveToken() function add the following.

context.dataStore.edit { prefs ->
        prefs[PreferencesKeys.TOKEN_KEY] = token
 }

Reading Data from Preferences DataStore
To read data from Preferences DataStore, use the corresponding key (that we defined earlier) call DataStore.data property to expose the appropriate store data values as a Flow.

override fun getToken(): Flow<String> = context.dataStore.data.catch { exception ->
        if (exception is IOException){
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }.map { it[PreferencesKeys.TOKEN_KEY] ?: "" }

Since I am using Koin for dependency injection, I am exposing the Preferences DataStore singleton instance in my ViewModel as below.

val prefsStoreModule = module {
    single<PreferencesStore> { PreferencesStoreImpl(androidContext()) }
}

Here is how my SignInViewModel looks like

  • Saving Token on SignIn
class SignInViewModel(
    private val authRepository: AuthRepository, private val preferencesStore: PreferencesStore
): ViewModel() {
   ...
    fun signInUser(signInRequest: SignInRequestBody){
        ...
        viewModelScope.launch {
            try {
                val response = authRepository.signIn(signInRequest)
                //Save the token
                prefsStore.saveToken(response.token)
                _signInViewState.value = ResultWrapper.Success(response)
            }
            catch (e: Exception) {
                ...
            }
        }
    }
}
  • Retrieving the token as Flow
    asLiveData() creates a LiveData containing the values collected from a Flow that can be observed from Views.
fun getToken(): LiveData<String> = prefsStore.getToken().asLiveData()

Migrating from SharedPrefereces to Preferences Data Store
DataStore lifts all the heavyweights by handling migrations for us. You only need to construct an instance of SharedPreferencesMigration that takes a Context and the name of the existing SharedPreferences, finally pass a list of the SharedPreferences instance to the produceMigrations constructor argument as shown below.

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
  name = STORE_NAME,
  produceMigrations = { context ->
       listOf(SharedPreferencesMigration(context, PREFERENCES_NAME))
  }
)

That's it for now.
Happy learning.