DataStore Tutorial For Android: Getting Started
In this tutorial you’ll learn how to read and write data to Jetpack DataStore, a modern persistance solution from Google. By Luka Kordić.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
DataStore Tutorial For Android: Getting Started
30 mins
- Getting Started
- Enabling Auto Import
- Implementing Theme Change
- Observing Theme Changes
- Introducing Jetpack DataStore
- Comparing Jetpack DataStore and SharedPreferences
- Migrating SharedPreferences to Preferences DataStore
- Creating an Abstraction for Prefs DataStore
- Creating Prefs DataStore
- Reading Data From Prefs DataStore
- Observing Values From Prefs DataStore
- Writing Data to DataStore
- Introducing Proto DataStore
- Preparing Gradle for Proto DataStore
- Creating Proto Files
- Defining Proto Objects
- Creating a Serializer
- Preparing ProtoStore
- Creating Proto DataStore
- Storing Filter Options
- Reading Filter Options
- Reacting To Filter Changes
- Where to Go From Here?
Introducing Proto DataStore
Proto DataStore uses protocol buffers to serialize data. Protocol buffers are Google’s language-neutral and platform-neutral mechanism for serializing structured data.
You define how you want your data structured once. Then you use special, generated source code to write and read your structured data to and from various data streams while using a variety of languages.
This tutorial only covers the code you need to implement filtering. For more information, visit this protocol buffers tutorial.
The first step to using a Proto DataStore is to prepare your Gradle files. Let’s do that!
Preparing Gradle for Proto DataStore
To work with Proto DataStore gradle files need:
- The Protobuf plugin
- The Protobuf and Proto DataStore dependencies
- Protobuf configuration
The protobuf plugin and dependencies are already in the project. Open the app-level build.gradle file and below plugins sections on the top, paste:
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.10.0"
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
Sync the project. You need all of these changes to enable code generation for the files you’ll write in the next section.
Creating Proto Files
Before you jump into the filtering option’s implementation, you need to create a file in which you define proto objects.
Switch to Project view in the Project pane on the left side of Android Studio. Create a proto directory in app/src/main. Inside the new directory, create a file named filter_options.proto.
Your structure will look like this:
Now that you have your proto file, you’ll define the proto objects.
Defining Proto Objects
To implement filtering, you need an object that holds filter data. >You’ll persist this object in Proto DataStore. You’ll describe how you want this object to look, and the Proto Buffer plugin will generate the code for you.
Open filter_options.proto and add:
syntax = "proto3"; option java_package = "com.raywenderlich.android.learningcompanion.data"; option java_multiple_files = true; message FilterOption { enum Filter { NONE = 0; BEGINNER = 1; ADVANCED = 2; COMPLETED = 3; BEGINNER_ADVANCED = 4; BEGINNER_COMPLETED = 5; ADVANCED_COMPLETED = 6; ALL = 7; } Filter filter = 1; }
The first line signals you’re using proto3 syntax. To learn more check out this protocol buffers documentation.
java_package
specifies where you want the compiler to put generated files. java_multiple_files
set to true
means the code generator will create a separate file for each top-level message.
In protobufs, you define every structure using a message keyword followed by its name. You define each member, or field, of the structure inside that message
. To define a field, you specify a type, name and unique number.
You can also put enums in a message. When defining enums, the first value always needs a unique number set to 0. Setting it to 0 tells the compiler you want this to be the default value.
For this use-case, you define eight enum values for eight possible filtering combinations. After you create Filter
enum, the last line of code above defines a field of type Filter
in FilterOption message
.
Build the project now by selecting Build ▸ Make Project to generate the classes you described. Nothing will change in the app at this point.
Now it’s time to create the serializer.
Creating a Serializer
To tell DataStore how to read and write the data type you define in filter_options.proto, you need Serializer
.
First, in java/learningcompanion create a new package called protostore which will hold all of the code related to Proto DataStore. Create a new Kotlin file in this package called FilterSerializer. Inside the new file add:
class FilterSerializer : Serializer<FilterOption> {
override fun readFrom(input: InputStream): FilterOption {
try {
return FilterOption.parseFrom(input)
} catch (e: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", e)
}
}
override fun writeTo(t: FilterOption, output: OutputStream) {
t.writeTo(output)
}
override val defaultValue: FilterOption = FilterOption.getDefaultInstance()
}
Import InvalidProtocolBufferException
from com.google.protobuf.
To create FilterSerializer
you implement the Serializer
and override its two functions: readFrom()
and writeTo()
. These two simple functions serialize and deserialize the objects you want to read and write.
The protobuf code generator generates parseFrom()
which you can use to deserialize objects. Similarly, it generates writeTo()
which you can use to serialize FilterOption
and write it to an OutputStream
. And finally, return the defaultValue
if there’s no data on disk.
Next, you’ll prepare the ProtoStore.
Preparing ProtoStore
Now that you’ve defined your filter object in a proto file and implemented serialization and deserialization mechanisms, it’s time to create an abstraction for the Proto DataStore. In protostore create a new Kotlin file called ProtoStore. Inside this file add:
interface ProtoStore {
val filtersFlow: Flow<FilterOption>
suspend fun enableBeginnerFilter(enable: Boolean)
suspend fun enableAdvancedFilter(enable: Boolean)
suspend fun enableCompletedFilter(enable: Boolean)
}
This interface exposes the currently selected filter through filtersFlow
. It also exposes three methods that let you enable or disable each of the filtering options. You mark each method with suspend
because you’ll have to call another suspend function in the implementation.
Now you’ll create the Proto DataStore instance.
Creating Proto DataStore
In protostore, create a new file called ProtoStoreImpl. Open the file and add:
class ProtoStoreImpl @Inject constructor(
@ApplicationContext private val context: Context) : ProtoStore {}
Here you create a class that implements ProtoStore
and uses Hilt to inject the Context
. When you create the class, Android Studio gives you an error saying you need to implement all of the methods from the interface.
Put your cursor on the class name and press Option-Return in macOS or Alt-Enter in Windows. In the pop-up that appears, select Implement members. Then, select all methods and press OK.
Leave the generated TODO
s for now. You’ll fix them in a moment.
To create a Proto DataStore, add the following code right below the class definition:
private val dataStore: DataStore<FilterOption> = context.createDataStore(
fileName = "courses.pb",
serializer = FilterSerializer()
)
Now import createDataStore()
with the Serializer
parameter.
This code creates a new DataStore
by using createDataStore()
. You pass in the name of a file where you’ll save data and a serializer you created in the previous step.
It’s finally time for you to save your selected filters.