Hello and welcome to another exciting tutorial! Today, we’ll delve into the world of Android Proto DataStore. If you’re still relying on SharedPreferences to store crucial values in your project, it’s strongly advised to make the switch to DataStore. In this tutorial, we’ll explore the fundamentals of Proto DataStore, a powerful tool for saving complex types. Let’s get started!
Table of Contents
For a detailed, step-by-step tutorial on utilizing Android Proto DataStore to store user session information, you can watch the accompanying video. Alternatively, if you prefer reading, continue with this post.
If you’ve landed here solely for the source code of this project, you can find it below.
Android Proto DataStore or Preferences Data Store
DataStore provides two options: Preferences and Proto DataStore. While this guide focuses on Proto DataStore, it’s essential to understand when to opt for Preferences DataStore. Preferences DataStore is akin to SharedPreferences, ideal for straightforward key-value pair persistence. On the other hand, if you need to persist typed objects to disk, Proto DataStore is the go-to choice.
Proto DataStore The Basics
Proto DataStore allows you to directly save Typed Objects, but it involves an additional step of creating an object schema using protobuf3 syntax.
Here’s a quick overview of the process for storing typed objects in Proto DataStore:
- Define your object schema using protobuf3 syntax.
- Create a DataStore object.
- Execute CRUD operations as needed.
Now, let’s put things into action. For this guide, feel free to use an existing Android project or create a new one.
Get the Dependencies on Place
To begin, you’ll need to acquire the DataStore dependency. The snippet below is specifically for Proto DataStore.
1 2 3 |
implementation("androidx.datastore:datastore-preferences:1.0.0") |
As mentioned earlier, to utilize Proto DataStore, class generation via protobuf schema is required. To facilitate this, we’ll need the following plugin and dependency.
First add the following plugin in your app-level build.gradle file.
1 2 3 4 5 6 7 8 9 |
plugins { id("com.android.application") id("org.jetbrains.kotlin.android") //add this plugin id("com.google.protobuf") version "0.9.4" } |
Then add the following dependency.
1 2 3 |
implementation("com.google.protobuf:protobuf-javalite:3.25.0") |
To complete the class generation process, add the following configuration at the end of your app-level build.gradle file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
protobuf { protoc { artifact = "com.google.protobuf:protoc:3.19.4" } generateProtoTasks { all().forEach { task -> task.plugins.create("java") { option("lite") } } } } |
Now you can sync your project with gradle files.
Creating a Proto Schema
I’ll be persisting a basic object containing userId and authKey.
1 2 3 4 5 6 7 8 9 10 11 |
syntax = "proto3"; option java_package = "net.simplifiedcoding.protodatastore"; option java_multiple_files = true; message User { int32 id = 1; string auth_key = 2; } |
Place this content within a .proto file, for instance, user.proto, located inside the app/main/proto directory. If the proto directory is not present by default, create it. For a visual guide, you can refer to the video mentioned above to walk through this step.
To gain a more comprehensive understanding of proto syntax, refer to the official guide. I won’t delve into it extensively here.
Ensure to rebuild your project, as class generation occurs only after this step.
Acquiring the DataStore Instance
Define a UserSerializer
Before you create the DataStore instance, make sure to set up a serializer for your generated proto class. In this case, we’re using the User class, and the serializer, represented as an object, is named UserSerializer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
object UserSerializer: Serializer<User> { override val defaultValue: User = User.getDefaultInstance() override suspend fun readFrom(input: InputStream): User { return User.parseFrom(input) } override suspend fun writeTo(t: User, output: OutputStream) { t.writeTo(output) } } |
DataStore Object
Creating a DataStore object is straightforward. Simply establish an extension property of type DataStore<YourType>Â within the Context. Delegate the initialization to the dataStore()Â function.
1 2 3 4 5 6 |
val Context.userDataStore: DataStore<User> by dataStore( fileName = "user.pb", serializer = UserSerializer ) |
Now, with the Context object, your DataStore is accessible from anywhere. Let’s delve into the detailed process of performing Create, Read, Update, and Delete (CRUD) operations on your DataStore. This will not only empower you to effectively manage data but also enhance the overall functionality of your application.
Create and Update
Creating and updating share a similar process. Whenever you update the data, the old value gets overwritten. Therefore, the steps to create and update are essentially the same.
1 2 3 4 5 6 7 8 |
context.userDataStore.updateData { it.toBuilder() .setAuthKey(authKey) .setId(id) .build() } |
Reading Persisted Values
1 2 3 4 5 6 |
context.userDataStore.data.map { //You can also map the values to a different object if required CurrentUser(it.id, it.authKey) } |
Deleting
1 2 3 4 5 |
context.userDataStore.updateData { it.toBuilder().clear().build() } |
The operations are straightforward and self-explanatory. Simply access your DataStore instance with the context and carry out the necessary operation based on your requirements.
Now, let’s explore a real-world example. In my Mini Tales project, I’ve employed Android Proto DataStore to store essential information about the logged-in user, specifically the user ID and authentication key. Allow me to walk you through the steps involved in implementing this functionality, providing insights into how the power of Proto DataStore enhances user data management in practical applications.
Creating a User Session Handler
When a user logs into my application, I leverage Proto DataStore to save the user information. This functionality is encapsulated in an interface named “SessionHandler,” appropriately named for its role in managing user sessions. Let’s proceed by creating the SessionHandler interface, as demonstrated below.
1 2 3 4 5 6 7 |
interface SessionHandler { suspend fun setCurrentUser(id: Int, authKey: String?) fun getCurrentUser(): Flow<CurrentUser> suspend fun clear() } |
Now I will create an implementation of this interface named DataStoreSesionHandler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
private val Context.userDataStore: DataStore<User> by dataStore( fileName = "user.pb", serializer = UserSerializer, ) class DataStoreSessionHandler @Inject constructor( @ApplicationContext private val context: Context, ) : SessionHandler { override suspend fun setCurrentUser(id: Int, authKey: String?) { context.userDataStore.updateData { it.toBuilder() .setAuthKey(authKey) .setId(id) .build() } } override fun getCurrentUser(): Flow<CurrentUser> { return context.userDataStore.data.map { CurrentUser(it.id, it.authKey) } } override suspend fun clear() { context.userDataStore.updateData { it.toBuilder().clear().build() } } } |
With the newly created DataStoreSessionHandler, we can seamlessly manage the session of a logged-in user within our application. This handler becomes a pivotal component for effectively handling and maintaining user sessions.
In a nutshell, diving into Android Proto DataStore with Kotlin makes handling data a breeze. You’ve learned the ropes, storing and retrieving typed objects effortlessly. That SessionHandler interface? It’s your go-to for managing user sessions. Give this guide a share with your developer buddies. Let’s level up our Android game together and make the most of Proto DataStore. Your shared knowledge makes us all stronger in the dev world! 🚀
Very well written and very nicely explained. Thanks for this post