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.
We are hoping that you are already having sufficient knowledge regarding when, why, and how to store data in Android applications using SharedPreferences
and Room
Database.
In this blog post, we will coverDataStore
, and its two types (Preferences DataStore, Proto DataStore).
At the end of this blog, you will learn how to use both these DataStores with the example given below:
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
Preferences DataStore: Stores and accesses data using Keys
. This implementation does not require a predefinedschema
and it does not provide type safety. Preferences DataStore uses key-value pairs to store and retrieve data.
The concept behind Preferences DataStore is similar to SharedPreferences
. If you are currently using SharedPreferences
in the project then at the end of this article you will be able to replace it with Preferences DataStore.
Proto DataStore: Stores data as instances of a custom data type. This implementation requires you to define schema
using protocol buffers, but it provides type safety. Proto DataStore returns the generated object in a Flow
.
The concept behind Proto DataStore is to store objects with custom data types.
implementation "androidx.datastore:datastore-preferences:1.0.0"
val Context.dataStore: DataStore<Preferences> by preferencesDataStore("user_prefs")
Here, we have usedpreferencesDataStore
, a property delegate that manages DataStore as a singleton. The mandatory parameter name(user_prefs) is the name of the Preferences DataStore.
val USER_AGE_KEY = intPreferencesKey("USER_AGE")
val USER_NAME_KEY = stringPreferencesKey("USER_NAME")
As the preferences DataStore does not use any kind ofschema
,
we need to use the key type function to define a key for each value that we need to store in the Database.
Here, we want to store user age and user name in the preferences DataStore and the corresponding key types will be intPreferencesKey()
and stringPreferencesKey()
.
suspend fun storeUserInfo(age: Int, name: String) {
context.dataStore.edit { preferences ->
preferences[USER_AGE_KEY] = age
preferences[USER_NAME_KEY] = name
}
}
Here, we have the suspended functionedit
()
which needs to be called from CoroutineContext
and inside the lambda, we have MutablePreferences
hence we can easily store/change the required values using keys.
Here, we should observe that we can easily get rid of using apply()
or commit()
functions to save changes (that we need to use in SharedPreferences
)
// get the user's age
val userAgeFlow: Flow<Int> = context.dataStore.data.map { preferences ->
preferences[USER_AGE_KEY] ?: 0
}
// get the user's name
val userNameFlow: Flow<String> = context.dataStore.data.map { preferences ->
preferences[USER_NAME_KEY] ?: ""
}
As the preferences DataStore exposes a Flow
representing the current state of data, we can easily get the Flow<Int>
and Flow<String>
for the user’s age and name respectively.
The complete class details for creating the database instance and reading/writing data are available on Github.
// Proto DataStore
implementation "androidx.datastore:datastore:1.0.0"
// protobuf
implementation "com.google.protobuf:protobuf-javalite:3.19.4"
Here, note that we have only taken dependency for Proto DataStore, if you want to use both Preferences DataStore and Proto DataStore in the same project kindly add the dependency for a Preferences DataStore also.
plugins {
...
id "com.google.protobuf" version "0.8.17"
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.19.4"
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
// for more information.
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
4. Add the proto file (Define a schema):
syntax = "proto3";
option java_package = "com.example.protodatastore";
option java_multiple_files = true;
message EmployeePreference{
string emp_name = 1;
string emp_designation = 2;
}
Here, we have defined a schema
called EmployeePreference which contains twofields
of string types called emp_name and emp_designation.
We need to place our .proto
file under app/source/main/proto folder. In our case file name is employee_pref.proto.
As you can see, each field in the message definition has a unique number. These field numbers are used to identify your fields in the message binary format, and should not be changed once your message type is in use.
As the class of the stored object is generated at compile time make sure to rebuild the project.
object EmployeePreferencesSerializer : Serializer<EmployeePreference> {
override val defaultValue: EmployeePreference = EmployeePreference.getDefaultInstance()
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun readFrom(input: InputStream): EmployeePreference {
try {
return EmployeePreference.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun writeTo(t: EmployeePreference, output: OutputStream) = t.writeTo(output)
}
The class EmployeePreferencesSerializer
implements Serializer<EmployeePreference>
where EmployeePreference
is the type defined in the proto file. This serializer
class tells DataStore how to read and write your data type.
private val Context.employeeProtoDataStore: DataStore<EmployeePreference> by dataStore(
fileName = DATA_STORE_FILE_NAME,
serializer = EmployeePreferencesSerializer
)
Use the property delegate created by dataStore
to create an instance of DataStore, where the type is the one 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 your 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 step 5.
suspend fun saveEmployeeInfo(eName: String, eDesignation: String) {
context.employeeProtoDataStore.updateData { employeeData ->
employeeData.toBuilder()
.setEmpName(eName)
.setEmpDesignation(eDesignation)
.build()
}
}
Proto DataStore provides 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.
//Reading employee object from a proto data store
val employeeInfo: Flow<EmployeePreference> = context.employeeProtoDataStore.data
.map {
it
}
//Reading employee object property empName from a proto data store
val empName: Flow<String> = context.employeeProtoDataStore.data
.map {
it.empName
}
//Reading employee object property empDesignation from a proto data store
val empDesignation: Flow<String> = context.employeeProtoDataStore.data
.map {
it.empDesignation
}
Here, we have used employeeProtoDataStore.data to read theFlow
of either the employee object or its properties like empName or empDesignation.
The complete class details for creating the proto-type database instance and reading/writing data are available on Github.
That’s it for today, hope you learned something!
Don’t worry if you didn’t get all the DataStore stuff.
You can find the complete source code of the preferences DataStore on Github.
You can find the complete source code of the proto DataStore on Github.
Whether you need...