Search code examples
androidserializationprotocol-buffers

How to store PersistentList<> in Proto datastore on android using kotlin


I'm trying to store class with List in proto datastore on android using kotlin by following the instruction in theType-Safe Preferences With Proto DataStore (without Protobuf Files!) - Full Guide on youtube. The class I want to store in the store looks like this:

@Serializable
data class QuizResult(
  val quizNameId:Int,
  val quizAboutId:Int,
  val answers: PersistentList<QuestionResult> = persistentListOf()
)

@Serializable
data class QuestionResult(
  val questionId:Int,
  val yesAnswered:Boolean
)

I have also serializer for the class:

object QuizResultSerializer : Serializer<QuizResult> {
  override val defaultValue: QuizResult
    get() = QuizResult(-1,-1)

  override suspend fun readFrom(input: InputStream): QuizResult {
    return try{
      Json.decodeFromString(
        deserializer = QuizResult.serializer(),
        string = input.readBytes().decodeToString()
      )
    }catch(e: SerializationException){
      e.printStackTrace()
      defaultValue
    }
  }

  override suspend fun writeTo(t: QuizResult, output: OutputStream) {
    output.write(
      Json.encodeToString(
        serializer = QuizResult.serializer(),
        value = t
      ).encodeToByteArray()
    )
  }
}

Database itself is defined in following way:

val Context.dataStore by dataStore("quiz-results.json",QuizResultSerializer)

I gather list of results in view model as mutableListOf<QuestionResult>() in results variable. When all answers are given, I'm trying to store it in the datastore with following code:

scope.launch {
  context.dataStore.updateData {
    it.copy(
      quizNameId = 123456,
      quizAboutId = 456789,
      answers = results.toPersistentList()
    )
  }
}

But when it's executed I'm getting runtime error stating that:

Process: com.example.redflagdetector, PID: 8535 kotlinx.serialization.SerializationException: Class 'SmallPersistentVector' is not registered for polymorphic serialization in the scope of 'PersistentList'. To be registered automatically, class 'SmallPersistentVector' has to be '@Serializable', and the base class 'PersistentList' has to be sealed and '@Serializable'.

When I comment out the line answers = results.toPersistentList() then there is no error and both quizNameId and quizAboutId are stored properly.

Any help why this is happening is greatelly appreciated.


Solution

  • This is due to the fact that in order to serialize Kotlin immutable collections, you have to write a custom serializer for the immutable collection that you are using.

    Therefore, in your case, you have to write a Custom PersistentList serializer similar to the one below:

    @OptIn(ExperimentalSerializationApi::class)
    @Serializer(forClass = PersistentList::class)
    class MyPersistentListSerializer(
        private val serializer: KSerializer<QuestionResult>,
    ) : KSerializer<PersistentList<QuestionResult>> {
    
        private class PersistentListDescriptor :
            SerialDescriptor by serialDescriptor<List<QuestionResult>>() {
            @ExperimentalSerializationApi
            override val serialName: String = "kotlinx.serialization.immutable.persistentList"
        }
    
        override val descriptor: SerialDescriptor = PersistentListDescriptor()
        
        override fun serialize(encoder: Encoder, value: PersistentList<QuestionResult>) {
            return ListSerializer(serializer).serialize(encoder, value)
        }
    
        override fun deserialize(decoder: Decoder): PersistentList<QuestionResult> {
            return ListSerializer(serializer).deserialize(decoder).toPersistentList()
        }
        
    }
    

    Then bind this serializer to your PersistentList inside your QuizResult data class using @Serializable annotation:

    @Serializable
    data class QuizResult(
      val quizNameId: Int,
      val quizAboutId: Int,
    
      @Serializable(with = MyPersistentListSerializer::class)
      val answers: PersistentList<QuestionResult> = persistentListOf()
    )
    

    And that's it! Now when you try to store your data class inside DataStore, you won't get an exception.

    Note: Do not confuse MyPersistentListSerializer with QuizResultSerializer, they each do separate things and you need to have both of them.

    Note: This Medium article is worth reading for anyone wondering how to serialize PersistentMap, or for anyone who wants to dig deeper on this topic.