Android Jetpack Proto DataStore Example

This blog is about Android Jetpack Proto DataStore and how to implement Proto DataStore in our Android application.

Proto DataStore

Proto DataStore stores data as objects of a custom data type. When using Proto DataStore, you have to define a schema using protocol buffers.

Creating New Project

1 . Create a new project by going to File  New Android Project, select Empty Activity , provide app name, select language to kotlin and then finally click on finish.

Adding dependencies

2 . To work with Proto DataStore and get Protobuf to generate code for our schema, we’ll have to make several changes to the build.gradle file:

  • Add the Protobuf plugin.
plugins {
    id "com.google.protobuf" version "0.8.12"
}
  • Configure Protobuf.
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.10.0"
}

generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
  • Now add the below dependencies inside dependencies block. Once you have added all these, sync your project.
dependencies {    
    // Proto DataStore
    implementation  "androidx.datastore:datastore-core:1.0.0-alpha01"
    implementation  "com.google.protobuf:protobuf-javalite:3.10.0"

    // LifeCycle
    implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
}

Defining a Schema

3 . To use Proto DataStore first we need to define a schema with proto file.

  • We will create a .proto file at app/src/main/proto  location. 
  • Now let’s define the schema.

data.proto

syntax = "proto3";

option java_package = "com.c1ctech.jetpackprotodatastore";
option java_multiple_files = true;

message Person {
string first_name = 1;
string last_name = 2;
int32 age = 3;
bool gender = 4;
}

4 . Open your activity_main.xml. To create the following UI, add the below code.

activity_main.xml

<?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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_60dp"
android:background="@color/colorPrimaryDark"
android:padding="@dimen/padding_18dp"
android:text="Save user"
android:textColor="@android:color/white"
android:textSize="@dimen/textsize_15sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/switch_gender"
app:layout_constraintStart_toStartOf="@+id/switch_gender"
app:layout_constraintTop_toBottomOf="@+id/switch_gender" />

<EditText
android:id="@+id/et_lname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_16dp"
android:ems="10"
android:hint="@string/hint_enter_last_name"
app:layout_constraintEnd_toEndOf="@+id/et_fname"
app:layout_constraintStart_toStartOf="@+id/et_fname"
app:layout_constraintTop_toBottomOf="@+id/et_fname" />

<EditText
android:id="@+id/et_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_16dp"
android:ems="10"
android:hint="@string/hint_enter_age"
android:inputType="number"
app:layout_constraintEnd_toEndOf="@+id/et_lname"
app:layout_constraintStart_toStartOf="@+id/et_lname"
app:layout_constraintTop_toBottomOf="@+id/et_lname"
tools:layout_editor_absoluteY="317dp" />

<EditText
android:id="@+id/et_fname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_30dp"
android:ems="10"
android:hint="@string/hint_enter_first_name"
app:layout_constraintEnd_toEndOf="@+id/tv_gender"
app:layout_constraintStart_toStartOf="@+id/tv_gender"
app:layout_constraintTop_toBottomOf="@+id/tv_gender"
tools:layout_editor_absoluteY="178dp" />

<TextView
android:id="@+id/tv_fname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_30dp"
android:textStyle="bold"
android:textColor="@color/colorPrimaryDark"
android:textSize="@dimen/textsize_20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tv_lname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_16dp"
android:textStyle="bold"
android:textColor="@color/colorPrimaryDark"
android:textSize="@dimen/textsize_20sp"
app:layout_constraintEnd_toEndOf="@+id/tv_fname"
app:layout_constraintStart_toStartOf="@+id/tv_fname"
app:layout_constraintTop_toBottomOf="@+id/tv_fname" />

<TextView
android:id="@+id/tv_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_16dp"
android:textStyle="bold"
android:textColor="@color/colorPrimaryDark"
android:textSize="@dimen/textsize_20sp"
app:layout_constraintEnd_toEndOf="@+id/tv_lname"
app:layout_constraintStart_toStartOf="@+id/tv_lname"
app:layout_constraintTop_toBottomOf="@+id/tv_lname" />

<TextView
android:id="@+id/tv_gender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_16dp"
android:textStyle="bold"
android:textColor="@color/colorPrimaryDark"
android:textSize="@dimen/textsize_20sp"
app:layout_constraintEnd_toEndOf="@+id/tv_age"
app:layout_constraintStart_toStartOf="@+id/tv_age"
app:layout_constraintTop_toBottomOf="@+id/tv_age" />

<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_gender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_16dp"
android:text="@string/female_male"
android:textSize="@dimen/textsize_20sp"
app:layout_constraintEnd_toEndOf="@+id/et_age"
app:layout_constraintStart_toStartOf="@+id/et_age"
app:layout_constraintTop_toBottomOf="@+id/et_age" />

</androidx.constraintlayout.widget.ConstraintLayout>

Create a Proto DataStore

5 . There are two steps involved in creating a Proto DataStore to store your typed objects:

1 . Create a class named MySerializer and write the following code.

object MySerializer : Serializer<Person> {

override fun readFrom(input: InputStream): Person {
try {
return Person.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}

override fun writeTo(t: Person, output: OutputStream) {
t.writeTo(output)
}

}
  • The class MySerializer implements Serializer<Person>, where Person is the type defined in the proto file. This Serializer class tells DataStore how to read and write your data type.

2. Create an instance of DataStore<Person>, where Person is the type defined in the proto file ( data.proto ). 

class PersonDataStore(context: Context) {
    private val dataStore: DataStore<Person>

    init {
        dataStore = context.createDataStore(
            fileName = "data.pb",
            serializer = MySerializer
        )
    }
  • filename :  tells DataStore which file to use to store the data.
  • serializer:  tells DataStore the name of the serializer class.

Read from a Proto DataStore

Use DataStore.data to read a Flow of the object or the appropriate property from your stored object.

//reading Person object
val user: Flow<Person> = dataStore.data
    .map {
        it
    }

//reading Person object property firstName
//val userName: Flow<String> = dataStore.data
   // .map {
     //   it.firstName
   // }

Write to a Proto DataStore

Proto DataStore provides an updateData() function that  gives you the current state of the data as an instance of your data type (Person) and update the stored object.

suspend fun storeData(age: Int, fname: String, lname: String, isMale: Boolean) {
dataStore.updateData { preference -> preference.toBuilder()
.setAge(age)
.setFirstName(fname)
.setLastName(lname)
.setGender(isMale).build()
}
}

Using Proto Data Store

6 . Open MainActivity.kt and add the below code.

package com.c1ctech.jetpackprotodatastore

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    private lateinit var personDataStore: PersonDataStore
    var age = 0
    var fname = ""
    var lname = ""
    var gender = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        personDataStore = PersonDataStore(this)


        //Gets the user input and saves it
        btn_save.setOnClickListener {
            fname = et_fname.text.toString()
            lname = et_lname.text.toString()
            age = et_age.text.toString().toInt()
            val isMale = switch_gender.isChecked

            //Stores the values
            lifecycleScope.launch {
                personDataStore.storeData(age, fname, lname, isMale)
            }

        }

        observeData()
    }

    private fun observeData() {

        personDataStore.user.asLiveData().observe(this, Observer {
            if (it != null && it.age > 0) {
                tv_fname.text = it.firstName
                tv_lname.text = it.lastName
                tv_age.text = it.age.toString()
                gender = if (it.gender) "Male" else "Female"
                tv_gender.text = gender

            } else {
                tv_fname.text = ""
                tv_lname.text = ""
                tv_age.text = ""
                tv_gender.text = ""
            }
        })
    }
}
  • In order to get the saved values in the activity from the DataStore, we need to change the Flow<Person> to LiveData with .asLiveData() and observe it.

Leave a Reply