Blog Infos
Author
Published
Topics
, , , ,
Published

A few years back, Google announced the DataStore, which is a replacement for the tried and true SharedPreferences. If you use or have used SharedPreferences in your application, you might be thinking of making the switch. But as with everything, the main question here is:

“What is going to be the cost in development”?

There are benefits for using DataStore, but only the Proto DataStore allows saving objects while providing type safety. If you look up at the documentation for Proto DataStore, you will find that it is a bit outdated and missing some crucial steps when working with it. So that is why, in this article, we are going to go over how to integrate Proto DataStore into your application and show that it’s not that big of a hassle to use it.

The What

Jetpack DataStore has two variants:

  • Preferences DataStore
  • Proto DataStore

We won’t be discussing the first, due to it’s similarity to SharedPreferences and also the fact that is has been covered widely. Let’s understand what the Proto in Proto DataStore means.

Proto is the name Google chose to represent Protocol Buffers. These are (Google’s) mechanism that help you in serializing structured data. They are not coding language specific and in general, you define the type of data that you wish to work with and then code is generated that helps you to read and write your data.

✋ We will be using the proto 3 version in this article

How does that definition look like?

message MyItem {
    string itemName = 1;
    int32 itemId = 2;
}

You define an object with the message keyword and inside of it you list the fields associated with that object. The numbers at the end of each field are used to identify the field itself and cannot be changed once being set and the object is in use.

But what if we wanted to have multiple objects in our .proto file? Assuming the objects are related to one another, this can done simply by adding more message objects:

message MyItem {
    string itemName = 1;
    int32 itemId = 2;
}

message MyListOfItems {
   repeated MyItem items = 1;
}

Notice that above we have added another message object that relies on the MyItem object defined above. If you want to define a list of objects, you need to use the repeated keyword.

The How

To get started, there is a need to add the following dependencies to your application level build.gradle:

implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation  "com.google.protobuf:protobuf-javalite:3.18.0"

Then, you will need to create a proto directory inside your project. This directory needs to be a sibling of the java folder in your project structure. Inside of the proto directory, you will be creating a .proto file. This file is responsible for generating the data types you wish to store in Proto DataStore.

Inside the proto directory, create a file with the .proto extension. Our .proto file will hold objects representing a Todo list (what else?). So we will call our file todo.proto and it will look like this:

syntax = "proto3";

option java_package = "com.yourPackageName.todo";
option java_multiple_files = true;

message TodoItem {
  string itemId = 1;
  string itemDescription = 2;
}

message TodoItems {
  repeated TodoItem items = 1;
}

Notice how we defined two message objects:

  1. TodoItem – that defines a todo item
  2. TodoItems – that defines a list of TodoItem objects

Next, build the project so that classes will be generated for TodoItem and TodoItems.

After our data objects have been defined, we need to create a class to serialize them. This class will tell the DataStore how to read/write our objects.

// 1
object TodoItemSerializer: Serializer<TodoItems> {
   // 2
    override val defaultValue: TodoItems = TodoItems.getDefaultInstance()
    // 3
    override suspend fun readFrom(input: InputStream): TodoItems {
        try {
            return TodoItems.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }
    // 3
    override suspend fun writeTo(
        t: TodoItems,
        output: OutputStream
    ) = t.writeTo(output)
}

Let’s review what we have in this class:

  1. When we declare the class, we need to implement the Serializer<T> interface with our object as the type (T)
  2. We define a default value for the serializer in case the file is not created
  3. We override the readFrom/writeTo methods and we make sure to have our object as the data type there

We have our .proto file with our data types and our serializer, so the next step is to instantiate the DataStore. We do this by using the property delegate created by dataStore, which requires giving a filename where our data will be saved and our serializer class (which we defined above).

private const val DATA_STORE_FILE_NAME = "todo.pb"

private val Context.todoItemDatastore: DataStore<TodoItems> by dataStore(
    fileName = DATA_STORE_FILE_NAME,
    serializer = TodoItemSerializer,
)

This piece of code needs to reside at the top of a class of your choosing above the definition of the class itself. I.E.

private const val DATA_STORE_FILE_NAME = "todo.pb"

private val Context.todoItemDatastore: DataStore<TodoItems> by dataStore(
    fileName = DATA_STORE_FILE_NAME,
    serializer = TodoItemSerializer,
)

class YourClassName {

}

To access this object in the rest of our application, we will need to use a context. An example is to use the application context in your viewmodel class:

class MyViewModel(application: Application): AndroidViewModel(application) {

   val todoDataStore = application.todoItemDataStore
   //...
}
The Flow

After we have gone through setting up everything we need for our DataStore, we need to discuss how we are actually going to interact with it. We will want to read and write data to/from it and the way we can do so is different from what you may be familiar with from SharedPreferences.

The DataStore we defined above has a data field that exposes a Flow for the properties we defined in our DataStore.

🚰 If you are not familiar with flows, this is a good place to start

val todoItemFlow: Flow<TodoItems> = todoItemDataStore.data
        .catch { exception ->
            if (exception is IOException) {
                emit(TodoItems.getDefaultInstance())
            } else {
                throw exception
            }
        }

The code above shows how you can define a Flow that collects data from the Proto DataStore. A catch block was added in case an exception occurs. You can place this logic in the class where you defined your DataStore and use it like so in your viewmodel :

val todoItemsFlow: LiveData<TodoItems> = todoItemsRepository.todoItemFlow.asLiveData()

Notice how we converted our Flow to LiveData. This was done for two reasons:

  1. Flows can stay active regardless of the activity/fragment that uses them
  2. As LiveData is something familiar to many developers and I wanted to make this example as approachable as possible

To be able to do this, you need to add the following dependency to your build.gradle file:

implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"

In your activity/fragment class, you can observe this live data like so:

myViewModel.todoItemFlow.observe(LocalLifecycleOwner.current) { todoItems ->
               // Logic to access data from DataStore
           }
The Why

After everything we reviewed, it’s time to talk about the elephant in the room. Should you go ahead and use DataStore (either Preferences or Proto) in your existing or next project?

In my opinion, the answer should be Yes. Besides the fact that Google is moving away from SharedPreferences, DataStore offers plenty of benefits to help you focus on your application and not the persistence of your data. It’s safe to interact with the DataStore from the UI thread (as it moves work to I/O automatically), it forces you to use Flow (if you haven’t still) and enjoy all the benefits within. There is also an option to migrate easily from SharedPreferences to Preferences DataStore, so there is really no excuse.

If you are contemplating using Room instead of Proto DataStore, well that depends on your use case. If the amount of data you are going to save (or persist) is rather small and won’t require partial updating, the Proto DataStore is the way to go. If you have a larger data set or one that may be complex, you should opt for using Room instead.

If you want to see how all this code looks like in an application, you can see it here:

GitHub – TomerPacific/Todo: Todo List application that uses Firebase to Authenticate users and save…

Todo List application that uses Firebase to Authenticate users and save data in a database. There is also an option to…

github.com

If you want to read other articles I have written, you can see them here:

GitHub – TomerPacific/MediumArticles: A repository that contains code associated with various…

A repository that contains code associated with various Medium articles I have written – GitHub …

github.com

References:

This article was previously published on proandroiddev.com

OUR VIDEO RECOMMENDATION

,

Combining Flutter with Protobuf to build a powerful mobile app

In this session, I will explain how to use Protobuf in a Flutter app to communicate between client-server. I will also discuss my learnings while using Protobuf in Flutter, and what are the pros and…
Watch Video

Combining Flutter with Protobuf to build a powerful mobile app

Angga Dwi Arifandi
Mobile Engineer
ING Netherlands

Combining Flutter with Protobuf to build a powerful mobile app

Angga Dwi Arifandi
Mobile Engineer
ING Netherlands

Combining Flutter with Protobuf to build a powerful mobile app

Angga Dwi Arifan ...
Mobile Engineer
ING Netherlands

Jobs

No results found.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu