Jetpack Proto DataStore with Kotlin generated classes for Proto schema
Jetpack DataStore allows us to store minimal key-value pairs or typed objects and acts as a replacement for SharedPreferences. It fixes all the downsides of SharedPreferences and uses Kotlin Flow to provide a flexible experience.
The official documentation is a good place to start learning about Jetpack DataStore.
Jetpack DataStore provides two ways to store and retrieve data.
- Store and retrieve values using keys. It has no type-safety.
- Store and retrieve values using custom-defined schema using protocol buffers
We are going to concentrate on the second one since it offers type-safety.
Let’s say we have an app to view movie list and movie details. And we want to save id
of the movie the user last visited.
Proto definition
We’ll define our proto schema as below and save it under src/main/proto
You have to create the
proto
folder manually if this is the first proto file you are placing in your project.And make sure you commit this proto file into Git.
To know about how to define data types, default values, and additional information about proto language, you can find the official guide here.
Gradle setup to generate Java files
Now we have to add the following in the app/module level build.gradle
file.
Now if you rebuild the project, a class named UserPreferences.java
would have been generated. It will have the variables we declared in the proto file and it will also have a nice Builder
class generated to create the UserPreferences
instance.
But we don’t use the Builder
pattern anymore since Kotlin came into the picture, thanks to the default arguments and copy function in the data class. So how do we generate Kotlin files for our proto schema?
Official Kotlin support for protobuf
I remember seeing about Kotlin support for protobuf in one of the videos from Google I/O that happened this year (2021).
In the protobuf gradle plugin Github repo, Kotlin support was mentioned as experimental. Still, I wanted to use it to see how it works. But I couldn’t any example code snippet to configure proto tasks in gradle to output Kotlin files.
I tried to tweak the default configuration to generate Java files to see if could generate Kotlin files from them, but it was a dead-end because I’m not an expert in gradle.
Wire gradle plugin to the rescue
Then I remember reading an article about Square’s Wire years ago before Kotlin became mainstream. Wire generated Java files alone then, but the main advantage I saw was, it didn’t generate unnecessary getters and setters which eliminated a lot of methods and it supported generating Java classes with Parcelable
implementation.
Lots happened between then and now. Now Wire supports generating Kotlin files from proto schema (wire started supporting Kotlin long back it seems).
So I tried using Wire to see how easy the setup is.
Here is the Wire’s guide to setup the gradle plugin to generate Java/Kotlin files from proto files.
And here is the configuration to generate Kotlin files from the proto files.
That’s it. On rebuilding the project, UserPreferences.kt
file would have been generated. This file is much smaller than the Java one we generated before.
It has no Builder
class, and no getter and setters. All fields are final (val
) and we can access them directly. To create a new instance, we can just call the constructor and it already has default arguments for all fields.
It also has a copy
function which replaces newBuilder()
method from the Java-generated files.
Plugging generated Kotlin classes to Jetpack DataStore
So how do we plug this into Jetpack DataStore?
The official documentation mentions that we need to create a class that extends Serializer
for our generated proto classes to let the DataStore know how to read/write our custom data type.
By using the above snippet mentioned in the documentation as a reference, we can create one for the Kotlin generated types.
Here are the things we needed to change from the Java class serializer type.
- The Kotlin classes generated by Wire don’t have
getDefaultInstance()
method. Instead, we can just use the constructor of the Kotlin class. If we don’t pass any parameter, it will use the default values for all fields and it will return a default instance for the type. parseFrom
andwriteTo
are named asdecode
andencode
respectively. And those methods will be inside anADAPTER
class for each type
That’s it. We generated Kotlin classes for our proto schema using Wire and plugged it into Jetpack DataStore. Now we can follow the steps mentioned in the official docs to read/write values using this Serializer
.