Datastore In Android

Introduction

Information can be retrieved and utilized later by an application. This information can be in various forms, such as text, images, or any other type of data the application requires. There are several data retrieval mechanisms available in Android, such as SharedPreferences, SQLite databases, and external file storage.

Prerequisites

To get along with this article, you should be familiarised with the following:-

What is datastore

The data store is a Jetpack library or API that stores small data sets in a thread-safe and non-blocking manner. It is aimed to replace Shared Preference and is built on Kotlin coroutines and Flows.

Implementation provided by datastore

Datastore provides mainly two implementation

  • Preference datastore
  • Proto datastore

    we shall talk more in detail about the two implementations.

A comparison between shared preference and datastore

Shared PrefPreference DatastoreProto Datastore
Async Api but does not update its value cause its UI blockingProvides safe Async Api using kotlin coroutine and flowsProvides safe Async Api using kotlin coroutine and flows
Supports Synchronous workDo operations in the background keeping the UI Thread unblockedDo operations in the background keeping the UI Thread unblocked
Throughs ErrorsProvides a way of handling the errors using flow signal mechanismProvides a way of handling the errors using flow signal mechanism
No type safetyNo type safetyProvide type safety
No data consistencyProvides data consistency and data is updated in read and write operationProvides data consistency and data is updated in read and write operation
No Migration supportProvides easy data migrationProvides easy data migration

Datastore vs Room

Choosing between Room and Datastore can be confusing. Here's a tip to help make the decision: check the size of the data you're working with. If it's complex and requires referential integrity and partial updates, go for Room. Otherwise, choose Datastore.

Let's get coding

"To demonstrate the implementation of the two data stores, we will store the app theme in our local storage such that when a different theme is selected, it changes the application appearance. First, we will begin with the Preference datastore, then proceed to the Proto datastore, and finally, compare the differences between the two."

Preference datastore

This is a data store implementation that stores values in key-value pairs. It provides quick migration from SharedPreferences and does not offer type safety. It also enables asynchronous operations using Kotlin flows and coroutines.

The following steps explain how to implement the Preference datastore.

Step 1 - Adding datastore dependency

In your build.gradle file project level add the following dependency

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

Step 2 - Naming Preference

Create an object class and give your Preference a name and also store your theme value

object Constants {

    const val PREFERENCE_NAME = "YOUR_PREFERENCE_NAME"
    val THEME_VALUE = intPreferencesKey(name = "theme_value")
}

Step 3 - Define your Data Preference class

create a class in your /data/local package and create your data preference class that saves the theme and also gets the theme.

class ThemePreference(
    private val dataStore: DataStore<Preferences>
) {

    val themeFlow: Flow<Int> = dataStore.data
        .map { preferences ->
            preferences[THEME_VALUE] ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
        }

    suspend fun saveTheme(theme: Int) {
        dataStore.edit { preferences ->
            preferences[THEME_VALUE] = theme
        }
    }
}

Step 4 - ViewModel

Now in your view model create a function that will save the theme and also get the theme as shown below

class ThemeViewModel(
    private val themePreference: ThemePreference
) : ViewModel() {

    val themeFlow: Flow<Int> get() = themePreference.themeFlow

    fun setTheme(theme: Int) {
        viewModelScope.launch {
            themePreference.saveTheme(theme)
        }
    }
}

Step 5 - Implementing the Ui

Since this focuses on changing the app theme all the UI operations will be done on the main activity which has several radio buttons and text views

  • Create the instance of theme preference and view model within the onCreate method.
  val themePreference = ThemePreference(dataStore = dataStore)
  val viewModel = ThemeViewModel(themePreference = themePreference)
  • Set a variable that collects the theme from the ViewModel and then set it to the Android theme
 val theme = viewModel.themeFlow.collectAsState(
                initial = Theme.FOLLOW_SYSTEM.themeValue,
                context = Dispatchers.Main.immediate
            ).value

make sure your theme composable takes a theme of type Int.

Thus set your theme like this

 DataStoresAndroidTheme(
                theme = theme
            ) {
                //codes here.
            }
  • Create a composable with radio buttons and text views and call it in your theme content body.

      @Composable
    fun AppThemeComponent(viewModel: ThemeViewModel) {
      Column(
          modifier = Modifier.fillMaxSize().padding(8.dp),
          verticalArrangement = Arrangement.Center,
          horizontalAlignment = Alignment.CenterHorizontally
      ) {
          Text(
              text = stringResource(R.string.choose_color),
              fontSize = 18.sp,
              fontWeight = FontWeight.Bold,
              fontFamily = FontFamily.Monospace,
              color = MaterialTheme.colorScheme.onBackground
          )
    
          val radioOptions = listOf<String>("Device settings", "Light Mode", "Dark Mode")
          val (selectedOption, onOptionSelected) = remember { mutableStateOf(radioOptions[0]) }
    
          Column(horizontalAlignment = Alignment.CenterHorizontally) {
              radioOptions.forEach { text ->
                  Row(
                      modifier = Modifier.fillMaxWidth().selectable(
                          selected = (text == selectedOption),
                          onClick = {
                              onOptionSelected(text)
                              when (text) {
                                  "Light Mode" -> {
                                      viewModel.setTheme(Theme.LIGHT_THEME.themeValue)
                                  }
                                  "Dark Mode" -> {
                                      viewModel.setTheme(Theme.DARK_THEME.themeValue)
                                  }
                                  else -> {
                                      viewModel.setTheme(Theme.FOLLOW_SYSTEM.themeValue)
                                  }
                              }
                          }
                      )
                          .padding(horizontal = 16.dp, vertical = 8.dp),
                      verticalAlignment = Alignment.CenterVertically
                  ) {
                      val context = LocalContext.current
    
                      RadioButton(
                          selected = (text == selectedOption),
                          modifier = Modifier.padding(all = Dp(value = 8F)),
                          onClick = {
                              onOptionSelected(text)
                              when (text) {
                                  "Light Mode" -> {
                                      viewModel.setTheme(Theme.LIGHT_THEME.themeValue)
                                  }
                                  "Dark Mode" -> {
                                      viewModel.setTheme(Theme.DARK_THEME.themeValue)
                                  }
                                  else -> {
                                      viewModel.setTheme(Theme.FOLLOW_SYSTEM.themeValue)
                                  }
                              }
                              Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
                          }
                      )
    
                      Text(
                          text = text,
                          modifier = Modifier.padding(start = 16.dp)
                      )
                  }
              }
          }
    
          Text(
              text = stringResource(R.string.device_settings),
              fontSize = 18.sp,
              fontWeight = FontWeight.Bold,
              fontFamily = FontFamily.Monospace,
              color = MaterialTheme.colorScheme.onBackground
          )
      }
    }
    

    Get the preference DataStore implementation from this GitHub repository

Proto data store

Proto data stores use type objects backed by protocal buffers which is type-safe. It relies on a flow error handling mechanism to handle errors. Proto data store provides Async operations using kotlin coroutines and flows. and also provide a quick data migration.

What is the protocol buffer

A protocol buffer is a language-neutral platform with the mechanism of serializing structured data similar to XML but it's faster, simpler, and easy to read.

Features of protocol buffer

  • language-neutral platform
  • serialize structured data
  • faster and smaller than xml
  • easy to read.

How to Implement proto data stores

Step 1 - Adding dependencies

in your build.gradle app module and add the protocol buffer id.

 id "com.google.protobuf" version "0.8.17"

Still on the same file, add the code below, which should not be in curly braces.

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.19.4"
    }

    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option 'lite'
                }
            }
        }
    }
}

Add the protobuf and data store dependencies

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

    // protobuf
    implementation  "com.google.protobuf:protobuf-javalite:3.19.4"

Sync your build,gradle.

Step 2- Define the structure of persisted data in proto file

Change the view of your project to project view then in app >src >main create a directory proto and in that directory create your .proto file in this case is theme_prefs.proto. In proto buff each structure is defined using a message keyword and each member of the structure is defined inside the message based on type and name and it is assigned a one-based order. For this case, my proto buff looks like this.

syntax = "proto3";

option java_package = "com.brandyodhiambo.datastoresandroid";
option java_multiple_files = true;

message ThemePreferences {

    int32 theme = 1;

}

The ThemePreference class is generated at a compile time from the message defined in proto thus make sure to rebuild the project.

Step 3 - Create the Serializer.

Move your project to android view and in your data,> local package create Yourserializer. kt file and extend Serializer<YourProtoBuffMessage>.The serializer implementation helps in telling the data store how to read and write the data type. It also defines the default value to be returned if there is no data on disk.

object ThemePreferenceSerializer : Serializer<ThemePreferences> {
    override val defaultValue: ThemePreferences = ThemePreferences.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): ThemePreferences {
        return ThemePreferences.parseFrom(input)
    }

    override suspend fun writeTo(t: ThemePreferences, output: OutputStream) {
        t.writeTo(output)
    }
}

Step 4 - Create Datastore

To create datastore use the DataStore<YourPreference> within the class to create a method to save data and a variable that reads the data from the datastore. for this case, I used a repository to act as my datastore class.

class ThemeRepository(
    private val themePreferencesDataStore: DataStore<ThemePreferences>
) {
    val themeFlow: Flow<Int> = themePreferencesDataStore.data
        .catch { exception ->
            if (exception is Exception) {
                emit(ThemePreferences.getDefaultInstance())
            } else {
                throw exception
            }
        }
        .map { themePreferences ->
            themePreferences.theme
        }

    suspend fun saveThemePreference(theme: Int) {
        themePreferencesDataStore.updateData { themePreference ->
            themePreference.toBuilder().setTheme(theme).build()
        }
    }
}

Create the ViewModel

The ViewModel implementation is similar to that of the preference Datastore. Here too you will create a method to save data and read data from the DataStore.

class ThemeViewModel(
    private val themeRepository: ThemeRepository
) : ViewModel() {

    val themeFlow: Flow<Int> get() = themeRepository.themeFlow

    fun setTheme(theme: Int) {
        viewModelScope.launch {
            themeRepository.saveThemePreference(theme)
        }
    }
}

The UI implementation is just similar to the one implemented above.

Get the proto DataStore implementation from this GitHub repository

Difference between preference datastore and proto

Prefrence DatastoreProto Datastore
Data is stored in a key-value pairuses protocol buffer to store data
Doesn't provide a type safetyProvides a type save
Gives a quick migration to shared prefAllows the use of complex data eg list and enum.

Conclusion

In conclusion, choosing the right data store is crucial for the success of an application or system. The right choice depends on specific requirements for scalability, performance, data model, data integrity, and security. Regular monitoring and adjustments are needed to ensure the chosen data store meets changing needs. The goal is to find a flexible, efficient, and well-suited data store.

References