Ultimate guide to jetpack store in android with example

Jetpack DataStore is a new and improved data storage solution aimed at replacing SharedPreferences. Built on Kotlin coroutines and Flow, DataStore provides two different implementations:

Data is stored asynchronouslyconsistently, and transactionally, overcoming most of the drawbacks of SharedPreferences. If you’re currently using SharedPreferences to store data, consider migrating to DataStore instead. Before getting started, check out another post on the jetpack,

Getting started with WorkManager [Example]

Dependency injection on Android with Hilt[Example]

Type Of DataStore

DataStore provides two different implementations:
Preferences DataStore — stores and accesses data using keys. This implementation does not require a predefined schema, and it does not provide type safety.
Proto DataStore — stores data as instances of a custom data type. This implementation requires you to define a schema using protocol buffers, but it provides type safety.

SharedPreferences vs DataStore

datastore comparison with Shared preference

SharedPreference blocked the UI thread on pending fsync() calls scheduled by apply(), often becoming a source of ANRs.

SharedPreferences throws parsing errors as runtime exceptions.

In both implementations, DataStore saves the preferences in a file and performs all data operations on Dispatchers.IO the thread.

Note: If you’re currently using SharedPreferences to store data, consider migrating to DataStore instead.

DataStore Vs Room

If you have a need for partial updates, referential integrity, or support for large/complex datasets, you should consider using Room instead of DataStore.

DataStore is ideal for small, simple datasets and does not support partial updates or referential integrity.

Using Preferences DataStore

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

Setup

To use Jetpack Preferences DataStore in your app, add the following to your Gradle file:

dependencies {
        // Preferences DataStore
        implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"
    }

Create a Preferences DataStore

Use the Context.createDataStore() extension function to create an instance of DataStore.

The mandatory name parameter is the name of the Preferences DataStore.

val preferenceDataStore: DataStore<Preferences> = createDataStore(name = "profile")

Read from a Preferences DataStore

Because Preferences DataStore does not use a predefined schema, you must use Preferences.preferencesKey() to define a key for each value that you need to store in the DataStore instance.

object PreferencesKeys {
        const val PREFERENCE_NAME = "profile"
        val USERNAME = preferencesKey<String>("username")
        val LOCATION = preferencesKey<String>("location")
        val AGE = preferencesKey<Int>("age")
        val IS_ACCOUNT_ACTIVE = preferencesKey<Boolean>("status")
    }

Then, use the DataStore.data property to expose the appropriate stored value using a Flow.

DataStore ensures that data is retrieved on Dispatchers.IO so your UI thread isn’t blocked.

CoroutineScope(Dispatchers.IO).launch {
                   preferenceDataStore.data.collect { profile ->
                       val username = profile[PreferencesKeys.USERNAME] ?: ""
                       val location = profile[PreferencesKeys.LOCATION] ?: ""
                        val age = profile[PreferencesKeys.AGE] ?: 0
                        val status = profile[PreferencesKeys.IS_ACCOUNT_ACTIVE] ?: false
                    }
                }

Handling exceptions in DataStore

As DataStore reads data from a file,IOExceptions are thrown when an error occurs while reading data. We can handle these by using the catch() Flow operator before map() .

preferenceDataStore.data.map { profile ->
                       val username = profile[PreferencesKeys.USERNAME] ?: ""
                       val location = profile[PreferencesKeys.LOCATION] ?: ""
                        val age = profile[PreferencesKeys.AGE] ?: 0
                        val status = profile[PreferencesKeys.IS_ACCOUNT_ACTIVE] ?: false
                    }.catch {exception ->
                       if(exception is IOException) {
                            //handle exception
                       } else {
                           throw exception
                       }
                   }

Write to a Preferences DataStore

To write data, DataStore offers a suspending DataStore.edit(transform: suspend (MutablePreferences) -> Unit) function, which accepts a transform block that allows us to transactionally update the state in DataStore.

All the code in the transform block is treated as a single transaction.

private suspend fun updateProfile(username: String, location: String, age: Int, status: Boolean) {

            preferenceDataStore.edit { profile ->
                profile[PreferencesKeys.USERNAME] = username
                profile[PreferencesKeys.LOCATION] = location
                profile[PreferencesKeys.AGE] = age
                profile[PreferencesKeys.IS_ACCOUNT_ACTIVE] = status
            }
        }

Migrate from SharedPreferences to Preferences DataStore

To migrate from SharedPreferences to DataStore, you need to pass in a SharedPreferencesMigration object to the DataStore builder. DataStore can automatically migrate from SharedPreferences to DataStore for you.

val preferenceDataStore: DataStore<Preferences> =
                createDataStore(
                    name = "profile",
                    migrations = listOf(SharedPreferencesMigration(this, "profile"))
                )

Using Proto DataStore

Proto DataStore lets you define a schema using Protocol buffers. Using Protobufs allows persisting strongly typed data. They are faster, smaller, simpler, and less ambiguous than XML and other similar data formats.

While Proto DataStore requires you to learn a new serialization mechanism, we believe that the strongly typed schema advantage brought by Proto DataStore is worth it.

Setup Proto Datastore

Start by adding the Proto DataStore dependency. If you’re using Proto DataStore, make sure you also add the proto dependency:

dependencies {
        implementation  "androidx.datastore:datastore-core:1.0.0-alpha01"
        implementation 'com.google.protobuf:protobuf-lite:3.0.0'
    }

Setup Protobuf

Add protopuf classpath in the project build.gradle:

classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'

add protopuf plugins to your app build.gradle:

apply plugin: 'com.google.protobuf'

    protobuf {
        protoc {
            artifact = 'com.google.protobuf:protoc:3.6.1'
        }
        plugins {
            javalite {
                artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0'
            }
        }
        generateProtoTasks {
            all().each { task ->
                task.builtins {
                    remove java
                }
                task.plugins {
                    javalite {}
                }
            }
        }
    }

    sourceSets {
        main.java.srcDirs += "${protobuf.generatedFilesBaseDir}/main/javalite"
        main.java.srcDirs += "$projectDir/src/main/proto"
    }

    processResources {
        exclude('**/*.proto')
    }

Also, make sure that you are using Java 1.8 version.

compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
        kotlinOptions {
            jvmTarget = '1.8'
        }

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 of objects that you persist in your Proto DataStore.

defined schema in a proto file

Once, create the proto file, android studio ask you to install a proto plugin to support the proto file format.

install protocol plugin

just, click install to add a proto plugin to install the plugin.

Note: The class for your stored objects is generated at compile time from the message defined in the proto file. Make sure you rebuild your project.

Create a Proto DataStore

Two steps involved in creating a Proto DataStore to store your typed objects:

Define a class that implements Serializer, where T is the type defined in the proto file. This serializer class tells DataStore how to read and write your data type.

val serializer = object : Serializer<Profile> {
                override fun readFrom(input: InputStream): Profile {
                    try {
                        return Profile.parseFrom(input)
                    } catch (exception: InvalidProtocolBufferException) {
                        throw CorruptionException("Cannot read proto.", exception)
                    }
                }

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

Use the Context.createDataStore() extension function to create an instance of DataStore, where T is the type defined in the proto file.

val protoDataStore: DataStore<Profile> = createDataStore(
                fileName = "profile.pb",
                serializer = serializer
            )

Read from a Proto DataStore

Use DataStore.data to expose a Flow of the appropriate property from your stored object.

val username: Flow<String> = protoDataStore.data
                .map { profile ->
                    // The exampleCounter property is generated from the proto schema.
                    profile.username
                }

Write to a Proto DataStore

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

private suspend fun updateProfile(username: String) {

            protoDataStore.updateData { profile ->
                profile.toBuilder()
                    .setUsername(username)
                    .build()
            }
        }

Migrate from SharedPreferences to Proto DataStore

To help with migration, DataStore defines the SharedPreferencesMigration class. The migrate block gives us two parameters:

*SharedPreferencesView allows us to retrieve data from SharedPreferences
*UserPreferences current data

We will have to return a UserPreferences object.

val sharedPrefsMigration = SharedPreferencesMigration(
                this@ProtoDataStoreActivity,
                "profile"
            ) { sharedPrefs: SharedPreferencesView, currentData: Profile ->
                // Define the mapping from SharedPreferences to UserPreferences
                currentData
            }

Now that we defined the migration logic, we need to tell DataStore that it should use it.

val dataStore: DataStore<Profile> = createDataStore(
                fileName = "user_prefs.pb",
                serializer = serializer,
                migrations = listOf(sharedPrefsMigration)
            )

You can download this example in GITHUB.

Thanks for reading. Please let me know your feedback in the comments. Share if you like it.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *


Latest Posts