Welcome Android Developers 👋. In the previous article, we saw how to use Preference DataStore to store key-value pairs. But it is not type-safe, Right? We’ll be covering that part in this article 😃. If you missed the previous article then you can read it 👇
Hello DataStore, Bye SharedPreferences - Android - Part 1 - Preference DataStore
What is Proto DataStore? 🤷♂
- It stores instances as custom data.
- Defines schema using Protocol buffers.
- They are faster, smaller, simpler, and less ambiguous than XML and other similar data formats.
That’s enough introduction for Proto DataStore to get started… It’s time to write some code👨💻😎.
Let’s begin code 👨💻
You can simply clone or refer this repository to get example code demonstrating DataStore.
What we’ll be implementing? So we’ll be developing a Food app 😋 where a list of food items will be displayed and we’ll provide a filter to the user for food preference options. Like Food type as 🟢VEG or 🔴NON-VEGand Food taste as SWEET or SPICY😋. So that user can filter his favourite food according to his choice. You can see a demo here. This is how it’ll be look alike👇.
Ok great! You might have got the idea of this app. First of all, we’ll need to add some dependencies for proto DataStore 👇
Add plugin and dependencies
Open build.gradle
of your app module and add plugin at the top of the file and proto Datastore, Google Protobuf dependencies and then configure protobuf.
plugins {
...
id "com.google.protobuf" version "0.8.12"
}
dependencies {
...
// Proto DataStore
implementation "androidx.datastore:datastore-core:1.0.0-alpha01"
// Protobuf
implementation "com.google.protobuf:protobuf-javalite:3.11.0"
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.11.0"
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
As you have seen above, we’ll have a list of food items. So let’s create a model for Food
data class Food(
val name: String,
val type: FoodType,
val taste: FoodTaste
)
enum class FoodTaste {
SWEET, SPICY
}
enum class FoodType {
VEG, NON_VEG
}
Here we’ll also create another class i.e. UserFoodPreference
which will be stored in DataStore. This model will be provided (or exposed) whenever preference is changed.
data class UserFoodPreference(
val type: FoodType?,
val taste: FoodTaste?
)
As you can see 👆 We’ve kept field nullable which means if they’re null then we can assume that user hasn’t set any filter or preference for food.
Define Protobuf
- Now we can’t directly store this model as it is using Proto DataStore. We’ll need to define a schema in a proto file.
- Create a new file called
food_preference.proto
in the pathapp/src/main/proto
as following 👇 (See this for syntax guide).
syntax = "proto3";
option java_package = "dev.shreyaspatil.datastore.example.proto";
option java_multiple_files = true;
message FoodPreferences {
enum FoodType {
TYPE_UNSPECIFIED = 0;
TYPE_VEG = 1;
TYPE_NON_VEG = 2;
}
enum FoodTaste {
TASTE_UNSPECIFIED = 0;
TASTE_SWEET = 1;
TASTE_SPICY = 2;
}
FoodType type = 1;
FoodTaste taste = 2;
}
As you can see 👆, we have created a schema in proto.
Whenever a value is not specified then
XXX_UNSPECIFIED
will be the default value here.
Once you’re done with the above step, Rebuild your Gradle project. You’ll see that FoodPreference.java
will be automatically generated from the above proto schema.
Make Serializer for Proto class
You’ll need to create a serializer for your proto generated class. Because Proto DataStore requires serializer field which serializes/deserializes proto objects. You can create it as follows 👇
object FoodPreferenceSerializer : Serializer<FoodPreferences> {
override fun readFrom(input: InputStream): FoodPreferences {
try {
return FoodPreferences.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override fun writeTo(t: FoodPreferences, output: OutputStream) = t.writeTo(output)
}
Create Food Preference Manager
Great! Now we’ll set up our Food Preference Manager which will store user food preferences (where we’ll actually implement Proto DataStore). Create a new class — FoodPreferenceManager
. Here you’ll need to provide a filename for DataStore as well as serializer object for proto which we created recently.
class FoodPreferenceManager(context: Context) {
private val dataStore: DataStore<FoodPreferences> =
context.createDataStore(
fileName = "food_prefs.pb",
serializer = FoodPreferenceSerializer
)
....
Now we’ll create two functions for setting or changing preferences.
- For Food type preference
suspend fun updateUserFoodTypePreference(type: FoodType?) {
val foodType = when (type) {
FoodType.VEG -> FoodPreferences.FoodType.TYPE_VEG
FoodType.NON_VEG -> FoodPreferences.FoodType.TYPE_NON_VEG
null -> FoodPreferences.FoodType.TYPE_UNSPECIFIED
}
dataStore.updateData { preferences ->
preferences.toBuilder()
.setType(foodType)
.build()
}
}
- For Food taste preference
suspend fun updateUserFoodTastePreference(taste: FoodTaste?) {
val foodTaste = when (taste) {
FoodTaste.SWEET -> FoodPreferences.FoodTaste.TASTE_SWEET
FoodTaste.SPICY -> FoodPreferences.FoodTaste.TASTE_SPICY
null -> FoodPreferences.FoodTaste.TASTE_UNSPECIFIED
}
dataStore.updateData { preferences ->
preferences.toBuilder()
.setTaste(foodTaste)
.build()
}
}
As you can see in the above methods. We have mapped our FoodType
or FoodTaste
to the proto generated enums. That’s all about setting or changing preferences.
Now we’ll need to expose a Flow
for observing user food preferences. We’ll map DataStore preferences to the data class which we earlier created i.e. UserFoodPreference
.
val userFoodPreference = dataStore.data.catch {
if (it is IOException) {
Log.e(TAG, "Error reading sort order preferences.", it)
emit(FoodPreferences.getDefaultInstance())
} else {
throw it
}
}.map {
val type = when (it.type) {
FoodPreferences.FoodType.TYPE_VEG -> FoodType.VEG
FoodPreferences.FoodType.TYPE_NON_VEG -> FoodType.NON_VEG
else -> null
}
val taste = when (it.taste) {
FoodPreferences.FoodTaste.TASTE_SWEET -> FoodTaste.SWEET
FoodPreferences.FoodTaste.TASTE_SPICY -> FoodTaste.SPICY
else -> null
}
UserFoodPreference(type, taste)
}
That’s all about FoodPreferenceManager
😃. Now let’s implement it in our UI.
Set up Activity
Here I’ll assume that you’re implementing this app with RecyclerView
in Activity
and you’ve implemented ViewModel
for getting data i.e. list of food items from Repository. (For demo purposes, here I’ve created a sample DataSource which gives a dummy list of food items). So I’ll directly show you implementation related to DataStore. You can refer to this class for more information.
So let’s make Activity
class ProtoDatastoreActivity : AppCompatActivity() {
private lateinit var foodPreferenceManager: FoodPreferenceManager
private val foodListAdapter by lazy { FoodListAdapter() }
Note: Here
foodListAdapter
is aRecyclerView.Adapter
’s implementation
Now, whenever Chips are clicked by the user then preferences should be stored or updated in DataStore. So let’s do that 👇
private fun initViews() {
foodTaste.setOnCheckedChangeListener { group, checkedId ->
val taste = when (checkedId) {
R.id.sweet -> FoodTaste.SWEET
R.id.spicy -> FoodTaste.SPICY
else -> null
}
lifecycleScope.launch { foodPreferenceManager.updateUserFoodTastePreference(taste) }
}
foodType.setOnCheckedChangeListener { group, checkedId ->
val type = when (checkedId) {
R.id.veg -> FoodType.VEG
R.id.nonVeg -> FoodType.NON_VEG
else -> null
}
lifecycleScope.launch { foodPreferenceManager.updateUserFoodTypePreference(type) }
}
}
Now we should be able to observe the changes in food preference so that we can filter the list accordingly. We already had exposed a Flow
which gives us UserFoodPreference
whenever it’s updated. So it’s time to use that 👀.
Note: Here we have used
asLiveData()
extension function ofFlow
. Otherwise, we can also uselifecycleScope
. This is generally useful when we’re actually implementing withViewModel
.
Now the implementation of filterFoodList()
should look like follows 👇
private fun filterFoodList(type: FoodType?, taste: FoodTaste?) {
var filteredList = getFoodList()
type?.let { foodType ->
filteredList = filteredList.filter { it.type == foodType }
}
taste?.let { foodTaste ->
filteredList = filteredList.filter { it.taste == foodTaste }
}
foodListAdapter.submitList(filteredList)
if (filteredList.isEmpty()) {
Toast.makeText(this, "No results!", Toast.LENGTH_SHORT).show()
}
updateViews(type, taste)
}
Yeah! That’s it 😍. We have implemented Proto DataStore now. If you run the app, you’ll see the output as you have seen at the starting of this article 🚀.
So that was about Proto DataStore. I hope you enjoyed this article or liked it! 😃.
Thank you! 😃