Android DataStore with proto for storing list items
Android Jetpack DataStore is the new alternative for SharedPreferences. It is a data storage solution that allows you to store key-value pairs or typed objects with protocol buffers. DataStore uses Kotlin coroutines and Flow to store data asynchronously, consistently, and transactionally. One of the advantage of using DataStore over SharedPreferences is that DataStore with protocol buffer language can store typed objects whereas SharedPreferences can only store primitive types. This post will list the steps needed to store a list of strings using DataStore with proto 3.
The dependencies and gradle configurations for the DataStore and proto 3 in the app gradle file.
plugins { id 'com.google.protobuf' version '0.8.17' } dependencies { implementation("androidx.datastore:datastore:1.0.0") implementation("androidx.datastore:datastore-preferences:1.0.0") implementation "com.google.protobuf:protobuf-javalite:3.19.4" } protobuf { protoc { artifact = "com.google.protobuf:protoc:3.19.4" } // Generates the java Protobuf-lite code for the Protobufs in this project. See // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation // for more information. generateProtoTasks { all().each { task -> task.builtins { java { option 'lite' } } } } }
Create a new folder at src/main/proto and create a proto file at src/main/proto/my_messages.proto with the following code. This is a very simple proto definition, in this case, it’s just defining an object type MyMessages with only one field, a list of strings. repeated makes the message a list of strings. java_package tells the compiler where it should put the class MyMessages. The following should generate a class at com.codexpedia.example.MyMessages
syntax = "proto3"; option java_package= "com.codexpedia.example"; option java_multiple_files = true; message MyMessages { repeated string message = 1; }
Create a Serializer class for reading and writing from the protocol buffer language, MyMessagesSerializer.kt
import androidx.datastore.core.CorruptionException import androidx.datastore.core.Serializer import com.google.protobuf.InvalidProtocolBufferException import java.io.InputStream import java.io.OutputStream import com.codexpedia.example.MyMessages /** * Serializer for the [MyMessages] object defined in my_messages.proto. */ object MyMessagesSerializer : Serializer{ override val defaultValue: MyMessages = MyMessages.getDefaultInstance() @Suppress("BlockingMethodInNonBlockingContext") override suspend fun readFrom(input: InputStream): MyMessages { try { return MyMessages.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } @Suppress("BlockingMethodInNonBlockingContext") override suspend fun writeTo(t: MyMessages, output: OutputStream) = t.writeTo(output) }
Create a class for making transactions(read, write, and remove) against the proto DataStore, MessageDataStore.kt
import android.util.Log import androidx.datastore.core.DataStore import com.codexpedia.example.MyMessages import kotlinx.coroutines.flow.catch import java.io.IOException import kotlinx.coroutines.flow.Flow class MessageDataStore private constructor(private val myMessagesStore: DataStore) { private val TAG: String = "MessageDataStore" val myMessagesFlow: Flow = myMessagesStore.data .catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading data if (exception is IOException) { Log.e(TAG, "Error reading sort order preferences.", exception) emit(MyMessages.getDefaultInstance()) } else { throw exception } } suspend fun saveNewMsg(newMsg: String) { myMessagesStore.updateData { myMessages -> if (myMessages.toBuilder().messageList.contains(newMsg)) { myMessages } else { myMessages.toBuilder().addMessage(newMsg).build() } } } suspend fun clearAllMyMessages() { myMessagesStore.updateData { preferences -> preferences.toBuilder().clear().build() } } suspend fun removeMsg(msg : String) { myMessagesStore.updateData { myMessages -> val existingMessages = myMessages.toBuilder().messageList if (existingMessages.contains(msg)) { val newList = existingMessages.filter { it != msg } myMessages.toBuilder().clear().addAllMessage(newList).build() } else { myMessages } } } companion object { @Volatile private var INSTANCE: MessageDataStore? = null fun getInstance(dataStore: DataStore ): MessageDataStore { return INSTANCE ?: synchronized(this) { INSTANCE?.let { return it } val instance = MessageDataStore(dataStore) INSTANCE = instance instance } } } }
Creating an instance of the proto DataStore using the serializer MyMessagesSerializer and DataStore class MessageDataStore
class MyApp : Application() { //This will make the myMessagesStore a singleton shared across the app private val Context.myMessagesStore: DataStoreby dataStore( fileName = "my_messages_store", serializer = MyMessagesSerializer, ) val messageDataStore: MessageDataStore get() = MessageDataStore.getInstance(myMessagesStore) }
To observe and get data from the proto DataStore.
// This line should be in a ViewModel val myMessages = messageDataStore.myMessagesFlow.asLiveData() //This should be in an Activity, Fragment or other UI view. myMessages.observe(viewLifecycleOwner) { Log.d("Debug", "messages: $it") }
To add a new message to the list of strings in proto DataStore. It needs to launch a coroutine because it was defined as a suspend function in MessageDataStore. It should be in ViewModel and use viewModelScope to create the coroutine.
viewModelScope.launch { messageDataStore.saveNewdMsg("a new message") }
To remove an item from the list.
viewModelScope.launch { messageDataStore.removeMsg("a message") }
To clear all list items from the DataStore.
viewModelScope.launch { messageDataStore.clearAllMyMessages() }
Search within Codexpedia
Search the entire web