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:
Preferences DataStore - stores and retrieves data using keys.
Does not provide type safety
Proto DataStore - stores data as instances of a custom data type.
Provides type safety with Protobuffers.
For most applications, Preferences DataStore is enough. Person objects are usually stored in the room database.
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")
}
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())
)
name
- The name of the preferences. The preferences will be stored in a file in the "datastore/" subdirectory in the application context's files directory and is generated using preferencesDataStoreFile.corruptionHandler
- The corruptionHandler is invoked if DataStore encounters a Exception.produceMigrations
- produce the migrations. The ApplicationContext is passed in to these callbacks as a parameter. DataMigrations are run before any access to data can occur.scope
- The scope in which IO operations and transform functions will execute.
Let's create objects containing the keys that we will use
companion object {
val IS_DARK_MODE = booleanPreferencesKey("dark_mode")
...
...
}
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
}
}
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
}
}
}
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
Sync the project to generate a java class for the DataStore according to the scheme from proto.
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;
}
syntax
- specifies that you’re using proto3 syntaxjava_package
- specifies where you want the compiler to put generated files.java_multiple_files
- set to true
means the code generator will create a separate file for each top-level message.message
- structure detection keyword1, 2, 3
identify the unique tag that the field uses in binary encoding.Useful plugin for proto files - Protocol Buffer Editor.
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()
}
defaultValue
- returns a default value if nothing is foundwriteTo
- how to convert for storage formatreadFrom
- reverse how to convert storage formatCreate 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.
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.
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()
}
}
toBuilder()
- gets the Builder
version of our person
.setFirstName(firstName)
- sets the new value.build()
- update by converting to PersonPreferences
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.