Posted by: Satya Pavan Kantamani
Introduction
SharedPreferences the common way used for storing key-value pairs in local storage. Datastore is a replacement to SharedPreferences to overcome its shortcomings. Datastore is an advanced data storage solution that was built using Kotlin coroutines and Flow to store data asynchronously, consistently, and transactionally. There are two ways to store data in DataStore. Those are Preferences DataStore and Proto DataStore. Please check out my previous post about introduction to Datastore and Preferences DataStore implementation
In this post let’s see we will know more about the following:
– Proto DataStore
– Protobufs
– Basic implementation of Proto DataStore
Familiarity with coroutines and Kotlin Flow is needed for understanding this.
If you want to jump directly to the code base, check out the GitHub repo.
What is Proto DataStore?
We are habituated with the system of a key-value pair for storing data in Shared preferences and Preferences DataStore. However, Proto DataStore stores the data as instances of a custom data type rather than key-value pairs. Proto DataStore needs a pre-defined schema with data types and instances that need to be stored. This schema approach helps in providing the type-safety with predefined data types. We need to Define a schema using Protocol buffers.
- Stores data as instances of a custom data type
- Defines the schema using Protocol buffers. Using Protobufs allows persisting strongly typed data.
- They are faster, smaller, simpler, and less ambiguous than XML and other similar data formats.
We need to follow a new serialization mechanism for Proto DataStore which we will see later in this post. Before jumping to the implementation let’s know a little bit about Protocol buffers.
What are Protocol buffers?
Protocol buffers mostly referred to as Protobufs are language and platform-neutral mechanisms of serializing data. Protobufs can be best suited for scenarios where faster communication over the network is needed or for storing data. Google created the ProtoBuf format in 2008. It’s an alternative solution to JSON, XML to serialize and deserialize data as fast as possible.
The current version of Protobuf is proto3. We will use this proto3 version later to create our proto datastore. It is important to know about the mechanism of Protobufs. In this mechanism, we use proto files with .
protoextensions where we write the data to be serialized. The proto files contain message types in which we define our data.
Let’s create a simple proto file with a message type in comparison with JSON type for better understanding. The simpler JSON file will be looking as below
{ is_logged_in: true, user_name: "Android" }
Now let’s create a proto file format for this
syntax = "proto3";message UserData{ bool is_logged_in = 1; string user_name = 2; }
For each field, we need to define Field Types
, Field Numbers
, Field Rules
,etc. To learn more about Protobufs checkout Google guide for Protobufs
Note: The proto files can be compiled to generate the code as per the user’s programming language.
Enough of talks let’s move to the coding part…
Implementation
Let’s create an UserDataStore where we store data related to the user.
Step 1
Adding a simple dependency is not sufficient here. We need to
- add the Protobuf plugin and configure the Protobuf
- add dependencies of Protobuf and Proto DataStore.
plugins { | |
... | |
id "com.google.protobuf" version "0.8.12" | |
} | |
dependencies { | |
implementation "androidx.datastore:datastore-core:1.0.0-rc01" | |
implementation "com.google.protobuf:protobuf-javalite:3.14.0" | |
... | |
} | |
protobuf { | |
protoc { | |
artifact = "com.google.protobuf:protoc:3.14.0" | |
} | |
// 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' | |
} | |
} | |
} | |
} | |
} |
Step 2
As we are done with Gradle set-up let’s move to the creation of a proto file with required fields. Create a proto directory under app/src/main/
. Let’s add the proto file to the proto directory with the name user_store
with the extension of .
proto .
Step 3: Adding the content in the proto file.
We need to define the version, package, java_multiple_files options and define our message object with the required fields. Let’s store two fields is_logged_in
of type Boolean and user_name
of type string. As we have already seen in the Protobuf section the syntax of the proto file let’s follow the same
syntax = "proto3"; option java_package = "com.sample.android_sample_preference_datastore"; option java_multiple_files = true; message UserStore { bool is_logged_in = 1; string user_name = 2; }
We can add multiple messages where each individual message has a class generated. If we have app-related data we can create AppData and for user-related stuff UserStore, etc
Step 4
Rebuild your project and check that UserStore.java should be generated under app/build/generated/source/proto
or double shift and check with the name UserStore
. Just have an overview look-up of UserStore
where it has different methods for storing, retrieving data, creating instances of UserStore
and other stuff.
Note: It would be best practice to run the blocks of readFrom and writeTo inside withContext(Dispatchers.IO) { } and handle exceptions in both the case as the methods readFrom and writeTo may throw IO Exception. We need to define multiple serializers if have multiple message types
// Generated by the protocol buffer compiler. DO NOT EDIT! | |
// source: user_store.proto | |
package com.sample.android_sample_preference_datastore; | |
/** | |
* Protobuf type {@code UserStore} | |
*/ | |
public final class UserStore extends | |
com.google.protobuf.GeneratedMessageLite< | |
UserStore, UserStore.Builder> implements | |
// @@protoc_insertion_point(message_implements:UserStore) | |
UserStoreOrBuilder { | |
private UserStore() { | |
userName_ = ""; | |
} | |
public static final int IS_LOGGED_IN_FIELD_NUMBER = 1; | |
private boolean isLoggedIn_; | |
/** | |
* <code>bool is_logged_in = 1;</code> | |
* @return The isLoggedIn. | |
*/ | |
@java.lang.Override | |
public boolean getIsLoggedIn() { | |
return isLoggedIn_; | |
} | |
/** | |
* <code>bool is_logged_in = 1;</code> | |
* @param value The isLoggedIn to set. | |
*/ | |
private void setIsLoggedIn(boolean value) { | |
isLoggedIn_ = value; | |
} | |
/** | |
* <code>bool is_logged_in = 1;</code> | |
*/ | |
private void clearIsLoggedIn() { | |
isLoggedIn_ = false; | |
} | |
public static final int USER_NAME_FIELD_NUMBER = 2; | |
private java.lang.String userName_; | |
/** | |
* <code>string user_name = 2;</code> | |
* @return The userName. | |
*/ | |
@java.lang.Override | |
public java.lang.String getUserName() { | |
return userName_; | |
} | |
/** | |
* <code>string user_name = 2;</code> | |
* @return The bytes for userName. | |
*/ | |
@java.lang.Override | |
public com.google.protobuf.ByteString | |
getUserNameBytes() { | |
return com.google.protobuf.ByteString.copyFromUtf8(userName_); | |
} | |
/** | |
* <code>string user_name = 2;</code> | |
* @param value The userName to set. | |
*/ | |
private void setUserName( | |
java.lang.String value) { | |
value.getClass(); | |
userName_ = value; | |
} | |
/** | |
* <code>string user_name = 2;</code> | |
*/ | |
private void clearUserName() { | |
userName_ = getDefaultInstance().getUserName(); | |
} | |
/** | |
* <code>string user_name = 2;</code> | |
* @param value The bytes for userName to set. | |
*/ | |
private void setUserNameBytes( | |
com.google.protobuf.ByteString value) { | |
checkByteStringIsUtf8(value); | |
userName_ = value.toStringUtf8(); | |
} | |
public static com.sample.android_sample_preference_datastore.UserStore parseFrom( | |
java.nio.ByteBuffer data) | |
throws com.google.protobuf.InvalidProtocolBufferException { | |
return com.google.protobuf.GeneratedMessageLite.parseFrom( | |
DEFAULT_INSTANCE, data); | |
} | |
public static com.sample.android_sample_preference_datastore.UserStore parseFrom( | |
java.nio.ByteBuffer data, | |
com.google.protobuf.ExtensionRegistryLite extensionRegistry) | |
throws com.google.protobuf.InvalidProtocolBufferException { | |
return com.google.protobuf.GeneratedMessageLite.parseFrom( | |
DEFAULT_INSTANCE, data, extensionRegistry); | |
} | |
public static com.sample.android_sample_preference_datastore.UserStore parseFrom( | |
com.google.protobuf.ByteString data) | |
throws com.google.protobuf.InvalidProtocolBufferException { | |
return com.google.protobuf.GeneratedMessageLite.parseFrom( | |
DEFAULT_INSTANCE, data); | |
} | |
public static com.sample.android_sample_preference_datastore.UserStore parseFrom( | |
com.google.protobuf.ByteString data, | |
com.google.protobuf.ExtensionRegistryLite extensionRegistry) | |
throws com.google.protobuf.InvalidProtocolBufferException { | |
return com.google.protobuf.GeneratedMessageLite.parseFrom( | |
DEFAULT_INSTANCE, data, extensionRegistry); | |
} | |
public static com.sample.android_sample_preference_datastore.UserStore parseFrom(byte[] data) | |
throws com.google.protobuf.InvalidProtocolBufferException { | |
return com.google.protobuf.GeneratedMessageLite.parseFrom( | |
DEFAULT_INSTANCE, data); | |
} | |
public static com.sample.android_sample_preference_datastore.UserStore parseFrom( | |
byte[] data, | |
com.google.protobuf.ExtensionRegistryLite extensionRegistry) | |
throws com.google.protobuf.InvalidProtocolBufferException { | |
return com.google.protobuf.GeneratedMessageLite.parseFrom( | |
DEFAULT_INSTANCE, data, extensionRegistry); | |
} | |
public static com.sample.android_sample_preference_datastore.UserStore parseFrom(java.io.InputStream input) | |
throws java.io.IOException { | |
return com.google.protobuf.GeneratedMessageLite.parseFrom( | |
DEFAULT_INSTANCE, input); | |
} | |
public static com.sample.android_sample_preference_datastore.UserStore parseFrom( | |
java.io.InputStream input, | |
com.google.protobuf.ExtensionRegistryLite extensionRegistry) | |
throws java.io.IOException { | |
return com.google.protobuf.GeneratedMessageLite.parseFrom( | |
DEFAULT_INSTANCE, input, extensionRegistry); | |
} | |
public static com.sample.android_sample_preference_datastore.UserStore parseDelimitedFrom(java.io.InputStream input) | |
throws java.io.IOException { | |
return parseDelimitedFrom(DEFAULT_INSTANCE, input); | |
} | |
public static com.sample.android_sample_preference_datastore.UserStore parseDelimitedFrom( | |
java.io.InputStream input, | |
com.google.protobuf.ExtensionRegistryLite extensionRegistry) | |
throws java.io.IOException { | |
return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry); | |
} | |
public static com.sample.android_sample_preference_datastore.UserStore parseFrom( | |
com.google.protobuf.CodedInputStream input) | |
throws java.io.IOException { | |
return com.google.protobuf.GeneratedMessageLite.parseFrom( | |
DEFAULT_INSTANCE, input); | |
} | |
public static com.sample.android_sample_preference_datastore.UserStore parseFrom( | |
com.google.protobuf.CodedInputStream input, | |
com.google.protobuf.ExtensionRegistryLite extensionRegistry) | |
throws java.io.IOException { | |
return com.google.protobuf.GeneratedMessageLite.parseFrom( | |
DEFAULT_INSTANCE, input, extensionRegistry); | |
} | |
public static Builder newBuilder() { | |
return (Builder) DEFAULT_INSTANCE.createBuilder(); | |
} | |
public static Builder newBuilder(com.sample.android_sample_preference_datastore.UserStore prototype) { | |
return (Builder) DEFAULT_INSTANCE.createBuilder(prototype); | |
} | |
/** | |
* Protobuf type {@code UserStore} | |
*/ | |
public static final class Builder extends | |
com.google.protobuf.GeneratedMessageLite.Builder< | |
com.sample.android_sample_preference_datastore.UserStore, Builder> implements | |
// @@protoc_insertion_point(builder_implements:UserStore) | |
com.sample.android_sample_preference_datastore.UserStoreOrBuilder { | |
// Construct using com.sample.android_sample_preference_datastore.UserStore.newBuilder() | |
private Builder() { | |
super(DEFAULT_INSTANCE); | |
} | |
/** | |
* <code>bool is_logged_in = 1;</code> | |
* @return The isLoggedIn. | |
*/ | |
@java.lang.Override | |
public boolean getIsLoggedIn() { | |
return instance.getIsLoggedIn(); | |
} | |
/** | |
* <code>bool is_logged_in = 1;</code> | |
* @param value The isLoggedIn to set. | |
* @return This builder for chaining. | |
*/ | |
public Builder setIsLoggedIn(boolean value) { | |
copyOnWrite(); | |
instance.setIsLoggedIn(value); | |
return this; | |
} | |
/** | |
* <code>bool is_logged_in = 1;</code> | |
* @return This builder for chaining. | |
*/ | |
public Builder clearIsLoggedIn() { | |
copyOnWrite(); | |
instance.clearIsLoggedIn(); | |
return this; | |
} | |
/** | |
* <code>string user_name = 2;</code> | |
* @return The userName. | |
*/ | |
@java.lang.Override | |
public java.lang.String getUserName() { | |
return instance.getUserName(); | |
} | |
/** | |
* <code>string user_name = 2;</code> | |
* @return The bytes for userName. | |
*/ | |
@java.lang.Override | |
public com.google.protobuf.ByteString | |
getUserNameBytes() { | |
return instance.getUserNameBytes(); | |
} | |
/** | |
* <code>string user_name = 2;</code> | |
* @param value The userName to set. | |
* @return This builder for chaining. | |
*/ | |
public Builder setUserName( | |
java.lang.String value) { | |
copyOnWrite(); | |
instance.setUserName(value); | |
return this; | |
} | |
/** | |
* <code>string user_name = 2;</code> | |
* @return This builder for chaining. | |
*/ | |
public Builder clearUserName() { | |
copyOnWrite(); | |
instance.clearUserName(); | |
return this; | |
} | |
/** | |
* <code>string user_name = 2;</code> | |
* @param value The bytes for userName to set. | |
* @return This builder for chaining. | |
*/ | |
public Builder setUserNameBytes( | |
com.google.protobuf.ByteString value) { | |
copyOnWrite(); | |
instance.setUserNameBytes(value); | |
return this; | |
} | |
// @@protoc_insertion_point(builder_scope:UserStore) | |
} | |
@java.lang.Override | |
@java.lang.SuppressWarnings({"unchecked", "fallthrough"}) | |
protected final java.lang.Object dynamicMethod( | |
com.google.protobuf.GeneratedMessageLite.MethodToInvoke method, | |
java.lang.Object arg0, java.lang.Object arg1) { | |
switch (method) { | |
case NEW_MUTABLE_INSTANCE: { | |
return new com.sample.android_sample_preference_datastore.UserStore(); | |
} | |
case NEW_BUILDER: { | |
return new Builder(); | |
} | |
case BUILD_MESSAGE_INFO: { | |
java.lang.Object[] objects = new java.lang.Object[] { | |
"isLoggedIn_", | |
"userName_", | |
}; | |
java.lang.String info = | |
"\u0000\u0002\u0000\u0000\u0001\u0002\u0002\u0000\u0000\u0000\u0001\u0007\u0002\u0208" + | |
""; | |
return newMessageInfo(DEFAULT_INSTANCE, info, objects); | |
} | |
// fall through | |
case GET_DEFAULT_INSTANCE: { | |
return DEFAULT_INSTANCE; | |
} | |
case GET_PARSER: { | |
com.google.protobuf.Parser<com.sample.android_sample_preference_datastore.UserStore> parser = PARSER; | |
if (parser == null) { | |
synchronized (com.sample.android_sample_preference_datastore.UserStore.class) { | |
parser = PARSER; | |
if (parser == null) { | |
parser = | |
new DefaultInstanceBasedParser<com.sample.android_sample_preference_datastore.UserStore>( | |
DEFAULT_INSTANCE); | |
PARSER = parser; | |
} | |
} | |
} | |
return parser; | |
} | |
case GET_MEMOIZED_IS_INITIALIZED: { | |
return (byte) 1; | |
} | |
case SET_MEMOIZED_IS_INITIALIZED: { | |
return null; | |
} | |
} | |
throw new UnsupportedOperationException(); | |
} | |
// @@protoc_insertion_point(class_scope:UserStore) | |
private static final com.sample.android_sample_preference_datastore.UserStore DEFAULT_INSTANCE; | |
static { | |
UserStore defaultInstance = new UserStore(); | |
// New instances are implicitly immutable so no need to make | |
// immutable. | |
DEFAULT_INSTANCE = defaultInstance; | |
com.google.protobuf.GeneratedMessageLite.registerDefaultInstance( | |
UserStore.class, defaultInstance); | |
} | |
public static com.sample.android_sample_preference_datastore.UserStore getDefaultInstance() { | |
return DEFAULT_INSTANCE; | |
} | |
private static volatile com.google.protobuf.Parser<UserStore> PARSER; | |
public static com.google.protobuf.Parser<UserStore> parser() { | |
return DEFAULT_INSTANCE.getParserForType(); | |
} | |
} | |
Step 5
Now it’s time to create the serializer. Let’s create a class that implements Serializer<T>
, where T
is the message type defined in the proto file. This Serializer
tells the datastore how to read and write the data type we defined in the proto file. Let’s create UserStoreSerializer
package com.sample.android_sample_preference_datastore.proto | |
import androidx.datastore.core.CorruptionException | |
import androidx.datastore.core.Serializer | |
import com.google.protobuf.InvalidProtocolBufferException | |
import com.sample.android_sample_preference_datastore.UserStore | |
import java.io.InputStream | |
import java.io.OutputStream | |
object UserStoreSerializer : Serializer<UserStore> { | |
override val defaultValue: UserStore = UserStore.getDefaultInstance() | |
override suspend fun readFrom(input: InputStream): UserStore { | |
try { | |
return UserStore.parseFrom(input) | |
} catch (exception: InvalidProtocolBufferException) { | |
throw CorruptionException("Cannot read proto.", exception) | |
} | |
} | |
override suspend fun writeTo(t: UserStore, output: OutputStream) = t.writeTo(output) | |
} |
Step 6: Creating the Proto DataStore instance
We can use the dataStore
delegate for the creation of Datastore instance. The delegate needs 2 mandatory inputs those are name and the serializer
import androidx.datastore.core.DataStore | |
import androidx.datastore.dataStore | |
private val USER_DATA_STORE_FILE_NAME = "user_store.pb" | |
val Context.userDataStore: DataStore<UserStore> by dataStore( | |
fileName = USER_DATA_STORE_FILE_NAME, | |
serializer = UserStoreSerializer | |
) |
The dataStore
delegate ensures that we have a single instance of DataStore with that name in our application.
Step 7: Read operation from the proto data store instance
Same as we did in the case of Preference Datastore we can make use of DataStore.data
to expose a Flow
of the specific property from the stored instance state.
override suspend fun getUserLoggedInState(): Flow<Boolean> { | |
return protoDataStore.data.map { protoBuilder -> | |
protoBuilder.isLoggedIn | |
} | |
} |
To handle exceptions while reading wrap with a catch block
override suspend fun getUserLoggedInState(): Flow<Boolean> { | |
return protoDataStore.data | |
.catch { exception -> | |
// dataStore.data throws an IOException when an error is encountered when reading data | |
if (exception is IOException) { | |
emit(UserStore.getDefaultInstance()) | |
} else { | |
throw exception | |
} | |
}.map { protoBuilder -> | |
protoBuilder.isLoggedIn | |
} | |
} |
Step 8: Wite operation on the proto data store instance
We have updatedata()
functions that update the data transactionally in an atomic read-modify-write operation. We fetch the current state of the property then write on it and save it.
override suspend fun saveUserLoggedInState(state: Boolean) { | |
protoDataStore.updateData {store -> | |
store.toBuilder() | |
.setIsLoggedIn(state) | |
.build() | |
} | |
} |
Job Offers
Example
Now let’s check them together in a simple example as we have done in the case of preference data store. Let’s store and fetch a boolean(user logged-in state)using a repository pattern inside an activity. In this example repository pattern is nothing but a simple interface and class implementing the interface where we do store and fetch operations on protoDataStore instance. For keeping this simple let’s do a manual injection of protoDataStore instance to repository implementation. As we keep observing the flowable emitted by the preference data store the data will be automatically changed
Step 1
Let’s create a simple XML with two buttons for log-in and log-out.
<?xml version="1.0" encoding="utf-8"?> | |
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:id="@+id/parent_layout" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
tools:context=".MainActivity"> | |
<TextView | |
android:id="@+id/txt_login_status" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:text="" | |
android:gravity="center" | |
android:textSize="40sp" | |
android:layout_marginBottom="30dp" | |
android:textColor="@color/white" | |
app:layout_constraintBottom_toTopOf="@+id/btn_login" | |
app:layout_constraintLeft_toLeftOf="parent" | |
app:layout_constraintRight_toRightOf="parent" | |
/> | |
<Button | |
android:id="@+id/btn_login" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:text="Login" | |
app:layout_constraintLeft_toLeftOf="parent" | |
app:layout_constraintTop_toTopOf="parent" | |
app:layout_constraintBottom_toBottomOf="parent"/> | |
<Button | |
android:id="@+id/btn_logout" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:text="Button" | |
app:layout_constraintRight_toRightOf="parent" | |
app:layout_constraintTop_toTopOf="parent" | |
app:layout_constraintBottom_toBottomOf="parent"/> | |
</androidx.constraintlayout.widget.ConstraintLayout> |
Step 2
Let’s create an Activity where we inflate the above layout and with one click of the Login button we save the state for user log-in as true and whereas on click of other we save false. Also, we create an instance of proto DataStore and will keep observing the user log-in state. Based on the state we append text and background-color
package com.sample.android_sample_preference_datastore.proto | |
import android.content.Context | |
import android.os.Bundle | |
import android.view.View | |
import android.widget.TextView | |
import androidx.appcompat.app.AppCompatActivity | |
import androidx.core.content.ContextCompat | |
import androidx.datastore.core.DataStore | |
import androidx.datastore.dataStore | |
import androidx.lifecycle.lifecycleScope | |
import com.sample.android_sample_preference_datastore.R | |
import com.sample.android_sample_preference_datastore.UserStore | |
import kotlinx.coroutines.Dispatchers | |
import kotlinx.coroutines.flow.collect | |
import kotlinx.coroutines.launch | |
import kotlinx.coroutines.withContext | |
class ProtoSampleActivity: AppCompatActivity() { | |
private val DATA_STORE_FILE_NAME = "user_store.pb" | |
val Context.userDataStore: DataStore<UserStore> by dataStore( | |
fileName = DATA_STORE_FILE_NAME, | |
serializer = UserStoreSerializer | |
) | |
private var userRepo : ProtoUserRepo?=null | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContentView(R.layout.activity_main) | |
userRepo = ProtoUserRepoImpl( userDataStore) | |
initListeners() | |
setDataToUI() | |
} | |
private fun setDataToUI() { | |
lifecycleScope.launch { | |
userRepo?.getUserLoggedInState()?.collect {state-> | |
withContext(Dispatchers.Main) { | |
updateUI(state) | |
} | |
} | |
} | |
} | |
private fun updateUI(state: Boolean) { | |
findViewById<TextView>(R.id.txt_login_status)?.text = | |
"User Logged-in state ${state}" | |
if(state){ | |
findViewById<View>(R.id.parent_layout)?.setBackgroundColor(ContextCompat.getColor(this,R.color.purple_200)) | |
}else{ | |
findViewById<View>(R.id.parent_layout)?.setBackgroundColor(ContextCompat.getColor(this,R.color.design_default_color_secondary)) | |
} | |
} | |
private fun initListeners() { | |
findViewById<View>(R.id.btn_login)?.setOnClickListener { | |
lifecycleScope.launch { | |
userRepo?.saveUserLoggedInState(true) | |
} | |
} | |
findViewById<View>(R.id.btn_logout)?.setOnClickListener { | |
lifecycleScope.launch { | |
userRepo?.saveUserLoggedInState(false) | |
} | |
} | |
} | |
} |
We need to add lifecycle-runtime-ktx dependency to make use of lifecycleScope
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
Step 3
Let’s create ProtoUserRepo with two methods for save and fetch of user-login state
package com.sample.android_sample_preference_datastore.proto | |
import kotlinx.coroutines.flow.Flow | |
interface ProtoUserRepo { | |
suspend fun saveUserLoggedInState(state:Boolean) | |
suspend fun getUserLoggedInState(): Flow<Boolean> | |
} |
Step 4
Let’s provide ProtoUserRepoImpl an implementation to ProtoUserRepo
package com.sample.android_sample_preference_datastore.proto | |
import androidx.datastore.core.DataStore | |
import com.sample.android_sample_preference_datastore.UserStore | |
import kotlinx.coroutines.flow.Flow | |
import kotlinx.coroutines.flow.catch | |
import kotlinx.coroutines.flow.map | |
import java.io.IOException | |
class ProtoUserRepoImpl(private val protoDataStore: DataStore<UserStore>) :ProtoUserRepo { | |
override suspend fun saveUserLoggedInState(state: Boolean) { | |
protoDataStore.updateData {store -> | |
store.toBuilder() | |
.setIsLoggedIn(state) | |
.build() | |
} | |
} | |
override suspend fun getUserLoggedInState(): Flow<Boolean> { | |
return protoDataStore.data | |
.catch { exception -> | |
// dataStore.data throws an IOException when an error is encountered when reading data | |
if (exception is IOException) { | |
emit(UserStore.getDefaultInstance()) | |
} else { | |
throw exception | |
} | |
}.map { protoBuilder -> | |
protoBuilder.isLoggedIn | |
} | |
} | |
} |
That’s all we are done now run the app and check the output
Output
If you have any issues while executing the code snippets please check out the GitHub repo for handy access.
Summary
Datastores are advancement solutions for storage. Proto Datastore uses proto buffers and offers type safety. Protobufs are for serializing and de-serializing the data. As it doesn’t have a stable release yet think twice before using it in apps. . So give it a try…
Thank you for reading…
Resources
More Android Articles
- Understand How View Renders in Android
- The Life Cycle of a View in Android
- Kotlin Series
- How to Implement In-App Purchases in Your Android App
- Many More
Tags: Android, AndroidDev, Programming, Kotlin
View original article at: