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: DataStore by 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

Custom Search

Search the entire web

Custom Search