DataStore
Before going into Proto DataStore, I will talk about DataStore. DataStore is a new and improved data storage solution aimed at replacing SharedPreferences. Built on top of Kotlin coroutines and Flow, DataStore offers two different implementations: Proto DataStore, which allows you to store imported objects (supported by Proto-protocal) and Preferences DataStore that stores in key pairs – value. Data is stored asynchronously, consistently and is transactional, overcoming some disadvantages of SharedPreferences.
In the scope of this article, I will only talk about Proto Data Store. People can refer to Google’s codelab about the DataStore Preferences here: https://codelabs.developers.google.com/codelabs/android-preferences-datastore/#0
A comparison table of the 3 types of SharedPreferences, Preference DataStore and Proto DataStore.
Proto DataStore
One of the downsides of SharedPreferences and DataStore Preferences is that there is no way to define a schema or to ensure that the keys are accessed with the correct type. Proto DataStore solves this problem by using Protocol buffers to define the schema. Using Proto DataStore knows what types are stored and will only deliver them, eliminating the need to use keys.
- It stores versions as custom data.
- Define the schema using the Protocol Buffer.
- They are faster, smaller, simpler, and less ambiguous than XML and other similar data formats.
Add Proto DataStore into Project:
This demo project is about arranging tasks according to the priority of the User chooses.
Go to ** build.gradle ** to add the code below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
plugins { ... id "com.google.protobuf" version "0.8.12" } dependencies { implementation "androidx.datastore:datastore-core:1.0.0-alpha01" implementation "com.google.protobuf:protobuf-javalite:3.10.0" ... } protobuf { protoc { artifact = "com.google.protobuf:protoc:3.10.0" } generateProtoTasks { all().each { task -> task.builtins { java { option 'lite' } } } } } |
Protocol buffers are a mechanism for serializing structured data. You define how you want your data to be structured once, and then the compiler generates the source code to make it easier to write and read the structured data. Create a Proto file: You define your schema in a proto file. In my project, I will create a file user_prefs.proto in the following path ** app / src / main / proto ** in this file with the following content: Each structure is defined by the keyword message and each member of The structure is defined within the message, based on the type and name, and it is assigned an order.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
syntax = "proto3"; option java_package = "com.codelab.android.datastore"; option java_multiple_files = true; message UserPreferences { // hiển thị hoặc ẩn task. bool show_completed = 1; // Xác định task sắp xếp theo ưu tiên nào. enum SortOrder { UNSPECIFIED = 0; NONE = 1; BY_DEADLINE = 2; BY_PRIORITY = 3; BY_DEADLINE_AND_PRIORITY = 4; } // thứ tự sắp xếp công việc do người dùng chọn. SortOrder sort_order = 2; } |
The UserPreferences
class is created at compile time from the message
specified in the proto file. Should rebuild when creating proto file.
Create serializer
In order for the DataStore to know how to read and write the data type that we specified in the proto file, we need to implement Serializer. Create a new file called UserPreferencesSerializer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package com.codelab.android.datastore.data import androidx.datastore.CorruptionException import androidx.datastore.Serializer import com.codelab.android.datastore.UserPreferences import com.google.protobuf.InvalidProtocolBufferException import java.io.InputStream import java.io.OutputStream object UserPreferencesSerializer : Serializer<UserPreferences> { override fun readFrom(input: InputStream): UserPreferences { try { return UserPreferences.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } override fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output) } |
If UserPreferences
or related methods are not found, Clean and Rebuild to make sure that Protobuf creates the object.
Create DataStore
Create the DataStore <UserPreferences>
in UserPreferencesRepository
based on the Context.createDataStore ().
extension method Context.createDataStore ().
The method has two required parameters:
- The name of the file where the DataStore will be active.
- Sequencer for the type to be used with the DataStore. In this project is
UserPreferencesSerializer
.
1 2 3 4 5 |
private val dataStore: DataStore<UserPreferences> = context.createDataStore( fileName = "user_prefs.pb", serializer = UserPreferencesSerializer) |
Read and write data from Proto DataStore
In UserPreferencesRepository will manage the reading and writing of data.
- Read data:
1 2 3 4 5 6 7 8 9 10 11 |
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data .catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading data if (exception is IOException) { Log.e(TAG, "Error reading sort order preferences.", exception) emit(UserPreferences.getDefaultInstance()) } else { throw exception } } |
- Data logging: To write data, DataStore provides a suspend DataStore.updateData () function.
1 2 3 4 5 6 |
suspend fun updateShowCompleted(completed: Boolean) { dataStore.updateData { preferences -> preferences.toBuilder().setShowCompleted(completed).build() } } |
Move from SharedPreferences
To help with migration, the DataStore
identifies the SharedPreferencesMigration
class. Create it in UserPreferencesRepository
. The moving block gives us two parameters:
SharedPreferencesView
allows us to retrieve data fromSharedPreferences.
- Current data of
UserPreferences
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
private val sharedPrefsMigration = SharedPreferencesMigration( context, USER_PREFERENCES_NAME ) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences -> // Define the mapping from SharedPreferences to UserPreferences if (currentData.sortOrder == SortOrder.UNSPECIFIED) { currentData.toBuilder().setSortOrder( SortOrder.valueOf( sharedPrefs.getString( SORT_ORDER_KEY,SortOrder.NONE.name)!! ) ).build() } else { currentData } } |
Stores the order to the DataStore
To update the sort order when enableSortByDeadlin()
and enableSortByPooter()
are called, we have to do the following: Call their respective functions in the lambda of dataStore.updateData ()
. Since UpdateData () is a function suspend
, should enableSortByDeadline()
and enableSortByPooter()
must also be made into a suspend
. Use existing UserPreferences
received from updateData()
to build a new sort order Update UserPreferences
by converting it to a generator, setting a new sort order, and then rebuilding the option. Here’s how the implementation of enableSortByDeadline()
looks like.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
suspend fun enableSortByDeadline(enable: Boolean) { dataStore.updateData { preferences -> val currentOrder = preferences.sortOrder val newSortOrder = if (enable) { if (currentOrder == SortOrder.BY_PRIORITY) { SortOrder.BY_DEADLINE_AND_PRIORITY } else { SortOrder.BY_DEADLINE } } else { if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) { SortOrder.BY_PRIORITY } else { SortOrder.NONE } } preferences.toBuilder().setSortOrder(newSortOrder).build() } } |
The functions enableSortByDeadline()
and enableSortByPooter()
are currently suspend function
so they will also be called in a new investigative program, launched in viewModelScope:
1 2 3 4 5 6 7 8 9 10 11 12 |
fun enableSortByDeadline(enable: Boolean) { viewModelScope.launch { userPreferencesRepository.enableSortByDeadline(enable) } } fun enableSortByPriority(enable: Boolean) { viewModelScope.launch { userPreferencesRepository.enableSortByPriority(enable) } } |
Summary:
SharedPreferences comes with a bunch of downsides – from a synchronous API that can prove safe to call on the UI chain, no error signaling mechanism, lack of a transaction API, and more. The DataStore is an alternative to SharedPreferences that addresses most of the shortcomings of the API. DataStore has a completely asynchronous API that uses Kotlin coroutines and Flow, handles data movement, ensures data consistency, and handles data errors.
Thank you for reading this article.
Link refer to the article
https://codelabs.developers.google.com/codelabs/android-proto-datastore
Source code:
https://github.com/nghiaptx-2124/Proto-DataStore/tree/master/android-datastore