Jetpack DataStore - A new way to store data on Android
Jetpack DataStore is a data storage solution that allows you to store key-value pairs or typed objects with protocol buffers. DataStore uses Kotlin coroutines and Flow to store data asynchronously, consistently, and transactionally.
If you're currently using SharedPreferences to store data, you should consider migrating to DataStore.
There are two different types of datastores:
1. Preferences DataStore?
Preferences DataStore stores and accesses data using keys. This implementation does not require a predefined schema, and it does not provide type safety.
2. Proto DataStore
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.
Implementation
To use Jetpack DataStore in your app, add required gradle dependency,
Preferences Datastore:
????// Preferences DataStore (SharedPreferences like APIs
????dependencies {
????????implementation "androidx.datastore:datastore-preferences:1.0.0"
???????}
Proto Datastore:
????// Typed DataStore (Typed API surface, such as Proto
????dependencies {
????????implementation "androidx.datastore:datastore:1.0.0"
????}
How to 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.
How to create a Preferences DataStore?
Use the property delegate created by preferencesDataStore to create an instance of Datastore<Preferences>. Call it once at the top level of your kotlin file, and access it through this property throughout the rest of your application. This makes it easier to keep your DataStore as a singleton. Alternatively, use RxPreferenceDataStoreBuilder if you're using RxJava. The mandatory name parameter is the name of the Preferences DataStore.
// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
Read from a Preferences DataStore
Because Preferences DataStore does not use a predefined schema, you have to use the corresponding key type function to define a key for each value that you need to store in the DataStore<Preferences> instance. For example, to define a key for an int value, use intPreferencesKey(). Then, use the DataStore.data property to expose the appropriate stored value using a Flow.
val TAP_COUNTER = intPreferencesKey("example_counter"
val counterFlow: Flow<Int> = context.dataStore.data
??.map { preferences ->
????// No type safety.
????preferences[TAP_COUNTER] ?: 0
}
How to write to a Preferences DataStore?
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 you can update the values as needed. All of the code in the transform block is treated as a single transaction.
suspend fun incrementCount()
??context.dataStore.edit { settings ->
????val currentCounterValue = settings[TAP_COUNTER] ?: 0
????settings[TAP_COUNTER] = currentCounterValue + 1
??}
}
How to store typed objects with Proto DataStore?
The Proto DataStore implementation uses DataStore and protocol buffers to persist typed objects to disk.
Define a schema:
To use Proto DataStore first we need to define a schema with protofile in the app/src/main/proto/ directory.
领英推荐
This schema defines the type for the objects that you persist in your Proto DataStore.?
To learn more about proto3 syntax, click here (https://developers.google.com/protocol-buffers/docs/proto3).
syntax = "proto3"
option java_package = "com.example.application";
option java_multiple_files = true;
message Settings {
??int32 example_counter = 1;
}
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.
How to create a Proto DataStore?
There are two steps involved in creating a Proto DataStore to store your typed objects:
1. Define a class that implements Serializer<T>, (T is the type defined in the proto file). This serializer class tells DataStore how to read and write your data type. Make sure you include a default value for the serializer to be used if there is no file created.
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 your kotlin file and access it through this property delegate throughout the rest of the app. 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 1st step.
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
)
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
Use DataStore.data to expose a Flow of the appropriate property from your stored object.
val counterFlow: Flow<Int> = context.settingsDataStore.dat
??.map { settings ->
????// The exampleCounter property is generated from the proto schema.
????settings.exampleCounter
??}
How to write to a Proto DataStore?
Proto DataStore provides an updateData() function that transactionally 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 operatsuspend fun
suspend fun incrementCount()
??context.settingsDataStore.updateData { currentSettings ->
????currentSettings.toBuilder()
??????.setExampleCounter(currentSettings.exampleCounter + 1)
??????.build()
????}
}{
Difference between SharedPreferences vs DataStore
- DataStore is built on Kotlin Coroutines and Flow. It exposes the preference values using Flow.
- You don’t need to manually switch to a background thread.
- In both implementations, DataStore saves the preferences in a file and performs all data operations on Dispatchers.IO unless specified otherwise.
- DataStore is safe from runtime exceptions and has error handling support, While SharedPreferences throws parsing errors as runtime exceptions.
Below table is the best way to differentiate between the two key-value pair based storage approaches, and the Proto DataStore:
How to use DataStore in synchronous code?
One of the most important bnefits of DataStore is that the asynchronous API, but it may not always be feasible to change your surrounding code to be asynchronous. This can happen, if you're working with an existing codebase that uses synchronous disk I/O or if you 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. You 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 exam
pleData = runBlocking { context.dataStore.data.first() }
Performing synchronous I/O operations on the UI thread can cause ANRs or UI jank. You can reduce these issues by asynchronouslypreloading data from DataStore:
override fun onCreate(savedInstanceState: Bundle?)
????lifecycleScope.launch {
????????context.dataStore.data.first()
????????// You should also handle IOExceptions here.
????}
}
Following this method, DataStore will read the data asynchronously 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.
By : Mihir Shah
Mihir Shah