What is DataStore?

Jetpack DataStore is a data storage solution that allows you to store key/value pairs or protocol buffers to store typed objects. DataStore is built on Kotlin and Flow coroutines, so all transactions are asynchronous, data storage and retrieval operations are safe and performant. It will replace SharedPreferences.

Jetpack DataStore is available in two types:

For most applications, Preferences DataStore is enough. Person objects are usually stored in the room database.

Preferences DataStore

Implementation Preferences DataStore

Add the dependencies in the build.gradle file for your app or module:

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

Create a Preferences DataStore

Creates a property delegate for a single process DataStore. This should only be called once in a file (at the top level), and all usages of the DataStore should use a reference the same Instance. The receiver type for the property delegate must be an instance of Context.

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = "settings",
    corruptionHandler = null,
    produceMigrations = { context ->
        emptyList()
    },
    scope = CoroutineScope(Dispatchers.IO + Job())
)

Creating keys

Let's create objects containing the keys that we will use

companion object {
        val IS_DARK_MODE = booleanPreferencesKey("dark_mode")
        ...
        ...
    }

Read from a Proto DataStore

To access data use DataStore.data. Add a catch block to handle errors.

The DataStore exposes stored data in the Preferences object.


enum class UiMode {
    LIGHT, DARK
}

val uiModeFlow: Flow<UiMode> = context.dataStore.data
        .catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throw it
            }
        }
        .map { preference ->
            // No type safety.
            when (preference[IS_DARK_MODE] ?: false) {
                true -> UiMode.DARK
                false -> UiMode.LIGHT
            }
        }

Write to a Preferences DataStore

Preferences DataStore provides an edit() function that transactionally updates the data in a DataStore. DataStore.edit() is a suspend and extension function on DataStore. All operations are safe and serialized. The coroutine will completes when the data has been persisted durably to disk

suspend fun setUiMode(uiMode: UiMode) {
        context.dataStore.edit { preferences ->
            preferences[IS_DARK_MODE] = when (uiMode) {
                UiMode.LIGHT -> false
                UiMode.DARK -> true
            }
        }
    }

Protocol Buffers

Implementation Proto DataStore

Add the dependencies in the build.gradle file for your app or module:

plugins {
    ...
    id "com.google.protobuf" version "0.8.18"
}
...
dependencies {
	  ...
    //DataStore
    implementation "androidx.datastore:datastore:1.0.0"
    // You need to depend on the lite runtime library, not protobuf-java
    implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
}

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.20.1'
    }

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

Check out the Protobuf Plugin for Gradle notes

Sync the project to generate a java class for the DataStore according to the scheme from proto.

Define a schema

The Proto DataStore implementation uses DataStore and protocol buffers to persist typed objects to disk. To learn more about defining a proto schema, see the protobuf language guide.

To describe the settings scheme, you need to create the proto file in the app/src/main/proto/ folder, a file with the *.proto extension

This schema defines the type for the objects that are to be persisted in Proto DataStore

syntax = "proto3";

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

message Person {
  int32 id = 1;
  string first_name = 2;
  string last_name= 3;
}

Useful plugin for proto files -  Protocol Buffer Editor.

Creating a Serializer

The serializer will take a stream of bytes and create a Person instance as a stream of bytes.

parse.

This Serializer object must be created as a singleton

object PersonSerializer : Serializer<Person> {
    override suspend fun readFrom(input: InputStream): Person {
        try {
            return Person.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("CorruptionException proto", exception)
        }
    }

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

    override val defaultValue: Person = Person.getDefaultInstance()
}

Create a Proto DataStore

Create a Proto DataStore instance by using createDataStore()

private val Context.dataStore: DataStore<Person> = context.createDataStore(
    fileName = "*.proto" ,
    serializer = PersonSerializer ,
    …
)

Use the fileName of the file where you will save the data.

Use the serializer we created earlier.

Reading

Get the data object: Flow<T>, which will return us the actual instance of the model stored in the DataStore:

val personFlow: Flow<Person> = dataStore.data

Do not layer a cache on top of this API: it will be be impossible to guarantee consistency. Instead, use data.first() to access a single snapshot.

Writing

Use the DataStore.updateData() suspend method to transactionally update data in an atomic read-modify-write operation. All operations are serialized, and the transformation itself is a coroutine.

suspend fun updateIsRegistered(firstName: String) {
    dataStore.updateData { personSettings ->
        person.toBuilder().setFirstName(firstName).build()
    }
}

Conclusion

DataStore is a replacement for SharedPreferences that fixes most of the shortcomings with the synchronous API, the lack of an error signaling mechanism, the lack of a transactional API, and others. DataStore gives us a very simple and convenient tool. If you need partial updates, referential integrity, or big data support, consider using the Room API instead of DataStore. The DataStore is used for small, simple data sets such as application settings and state. It does not support partial updates: if any field changes, the entire object will be serialized and saved to disk.