Android – An Overview Of Jetpack DataStore

Hello Readers, CoolMonkTechie heartily welcomes you in this article (An Overview Of Jetpack DataStore).

In this article, we will learn about Google’s new library Jetpack DataStore in Android. Jetpack DataStore is Google’s new library to persist data as key-value pairs or typed objects using protocol buffers. Using Kotlin coroutines and Flow as its foundation, it aims to replace SharedPreferences. This is part of the Jetpack suite of libraries. This article explains about Jetpack DataStore types implementations and address the limitations of the SharedPreferences API in Android.

To understand the Jetpack DataStore, we cover the below topics as below :

  • Overview
  • Limitation of SharedPreferences
  • Types of Jetpack DataStore Implementations
  • Jetpack DataStore Setup
  • Store key-value pairs with Preferences DataStore
  • Store typed objects with Proto DataStore
  • Use Jetpack DataStore in synchronous code

A famous quote about learning is :

“One learns from books and example only that certain things can be done. Actual learning requires that you do those things.”

So Let’s begin.

Overview

Jetpack DataStore is a data storage solution that allows us to store key-value pairs or typed objects with protocol buffers. DataStore uses Kotlin coroutines and Flow to store data asynchronously, consistently, and transactionally. Google introduced DataStore to address the limitations in the SharedPreferences API.

Limitation of SharedPreferences

To understand the DataStore’s advantages, we need to know about the limitations of SharedPreferences API. Even though SharedPreferences has been around since API level 1, it has drawbacks that have persisted over time:

  • SharedPreferences is not always safe call on the UI thread. It can cause junk by blocking the UI thread.
  • There is no way for SharedPreferences to signal errors except for parsing errors as runtime exceptions.
  • SharedPreferences has no support for data migration. If we want to change the type of a value, we have to write the entire logic manually.
  • SharedPreferences doesn’t provide type safety. Application will compile fine If we try to store both Booleans and Integers using the same key.

Google introduced DataStore to address the above limitations.

Types of Jetpack DataStore Implementations

DataStore provides two different implementations: Preferences DataStore and Proto DataStore.

  • Preferences DataStore: Stores and accesses data using keys. This implementation does not require a predefined schema, and it does not provide type safety. This is similar to SharedPreferences. We use this to store and retrieve primitive data types.
  • Proto DataStore: Uses protocol buffers to store custom data types. When using Proto DataStore, we need to define a schema for the custom data type.

SharedPreferences uses XML to store data. As the amount of data increases, the file size increases dramatically and it’s more expensive for the CPU to read the file.

Protocol buffers are a new way to represent structured data that’s faster and than XML and has a smaller size. They’re helpful when the read-time of stored data affects the performance of our application.

Jetpack DataStore Setup

To use Jetpack DataStore in application, we add the following dependencies to Gradle file depending on which implementation we want to use:

Datastore Typed

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation("androidx.datastore:datastore:1.0.0")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.0.0")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-rxjava3:1.0.0")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-core:1.0.0")
    }
    

Datastore Preferences

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation("androidx.datastore:datastore-preferences:1.0.0")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.0.0")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-preferences-rxjava3:1.0.0")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-preferences-core:1.0.0")
    }
    

If we use the datastore-preferences-core artifact with Proguard, we must manually add Proguard rules to our proguard-rules.pro file to keep our fields from being deleted.

Store key-value pairs with Preferences DataStore

The Preferences DataStore implementation uses the DataStore and Preferences classes to persist simple key-value pairs to disk.

Create a Preferences DataStore

// At the top level of our kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

Use the property delegate created by preferencesDataStore to create an instance of Datastore<Preferences>. Call it once at the top level of kotlin file, and access it through this property throughout the rest of application. This makes it easier to keep DataStore as singleton.

Read from a Preferences DataStore

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}

Because Preferences DataStore does not use a predefined schema, we must use the corresponding key type function to define a key for each value that we need to store in the DataStore<Preferences> instance.

Write to a Preferences DataStore

suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}

Preferences DataStore provides an edit() function that transactionally updates the data in a DataStore. The function’s transform parameter accepts a block of code where we can update the values as needed. All of the code in the transform block is treated as a single transaction.

Store typed objects with Proto DataStore

The Proto DataStore implementation uses DataStore and protocol buffers to persist typed objects to disk.

Define a schema

Proto DataStore requires a predefined schema in a proto file in the app/src/main/proto/ directory. This schema defines the type for the objects that we persist in our Proto DataStore.

syntax = "proto3";

option java_package = "com.example.application";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

The class for our stored objects is generated at compile time from the message defined in the proto file. Make sure we rebuild our project.

Create a Proto DataStore

There are two steps involved in creating a Proto DataStore to store typed objects:

  • Step 1 : Define a class that implements Serializer<T>, where T is the type defined in the proto file. This serializer class tells DataStore how to read and write data type. Make sure we include a default value for the serializer to be used if there is no file created yet.
  • Step 2 : Use the property delegate created by dataStore to create an instance of DataStore<T>, where T is the type defined in the proto file. Call this once at the top level of kotlin file and access it through this property delegate throughout the rest of application. The filename parameter tells DataStore which file to use to store the data, and the serializer parameter tells DataStore the name of the serializer class defined in step 1.
object SettingsSerializer : Serializer<Settings> {
  override val defaultValue: Settings = Settings.getDefaultInstance()

  override suspend fun readFrom(input: InputStream): Settings {
    try {
      return Settings.parseFrom(input)
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException("Cannot read proto.", exception)
    }
  }

  override suspend fun writeTo(
    t: Settings,
    output: OutputStream) = t.writeTo(output)
}

val Context.settingsDataStore: DataStore<Settings> by dataStore(
  fileName = "settings.pb",
  serializer = SettingsSerializer
)

Read from a Proto DataStore

We use DataStore.data to expose a Flow of the appropriate property from our stored object.

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }

Write to a Proto DataStore

Proto DataStore provides an updateData() function that transactionally updates a stored object. updateData() gives us the current state of the data as an instance of our data type and updates the data transactionally in an atomic read-write-modify operation.

suspend fun incrementCounter() {
  context.settingsDataStore.updateData { currentSettings ->
    currentSettings.toBuilder()
      .setExampleCounter(currentSettings.exampleCounter + 1)
      .build()
    }
}

Use Jetpack DataStore in synchronous code

Asynchronous API is one of the primary benefits of DataStore. It may not always be feasible to change our surrounding code to be asynchronous. This might be the case if we’re working with an existing codebase that uses synchronous disk I/O or if we have a dependency that doesn’t provide an asynchronous API.

Kotlin coroutines provide the runBlocking() coroutine builder to help bridge the gap between synchronous and asynchronous code. We can use runBlocking() to read data from DataStore synchronously. RxJava offers blocking methods on Flowable. The following code blocks the calling thread until DataStore returns data:

val exampleData = runBlocking { context.dataStore.data.first() }

Performing synchronous I/O operations on the UI thread can cause ANRs or UI junk. We can mitigate these issues by asynchronously preloading the data from DataStore:

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}

This way, DataStore asynchronously reads the data and caches it in memory. Later synchronous reads using runBlocking() may be faster or may avoid a disk I/O operation altogether if the initial read has completed.

That’s all about in this article.

Conclusion

In this article, we understood about Google’s new library Jetpack DataStore in Android. This article explained about Jetpack DataStore types implementations and address the limitations of the SharedPreferences API in Android.

Thanks for reading! I hope you enjoyed and learned about Jetpack DataStore concepts in Android. Reading is one thing, but the only way to master it is to do it yourself.

Please follow and subscribe to the blog and support us in any way possible. Also like and share the article with others for spread valuable knowledge.

You can find other articles of CoolMonkTechie as below link :

You can also follow the official website and tutorials of Android as below links :

If you have any comments, questions, or think I missed something, leave them below in the comment box.

Thanks again Reading. HAPPY READING !!???

Loading

Summary
Android - An Overview Of Jetpack DataStore
Article Name
Android - An Overview Of Jetpack DataStore
Description
This article explains about Jetpack DataStore types implementations and address the limitations of the SharedPreferences API in Android.
Author

Leave a Comment