1

I have this Firestore document Quiz_android that looks list this:

enter image description here

It is a simple array with maps in it. Now I would like to bind those results to some objects in Kotlin. Therefore I have made the following:

data class QuizBody(
    val questions: List<Question>
)

data class Question(
    val question: String,
    val answers: List<String>,
    val answer: Int
)

A Quizbody is just all the questions for the quiz in a list, and in that list, I have classes of Question which should be able to store all the data from the call.

But how do I bind the result from the call to those objects?

    suspend fun getQuestions(quizToGet: String) {
        try {
            //firestore has support for coroutines via the extra dependency we've added :)
            withTimeout(5_000) {
                firestore.collection("Quizzes").document(quizToGet).get()
                    .addOnCompleteListener { task ->
                        if (task.isSuccessful) {
                            val result = task.result
                            if (result.exists()) {

                                val myObject = result.toObject(QuizBody::class.java)
                                println(myObject)
                            }
                        }
                    }
            }
        } catch (e: Exception) {
            throw QuizRetrievalError("Retrieving a specific quiz was unsuccessful")
        }
    }

I have made this but this does not work.

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.hva.madlevel7task2, PID: 3995
    java.lang.RuntimeException: Could not deserialize object. Class com.hva.madlevel7task2.model.QuizBody does not define a no-argument constructor. If you are using ProGuard, make sure these constructors are not stripped

Edit:

I have updated the data class:

data class QuizBody(
    var questions: List<Question>? = null
)

data class Question(
    var question: String? = null,
    var answers: List<String>? = null,
    var answer: Int? = null
)

suspend fun getQuestions(quizToGet: String) it still the same, now I get this in the console:

I/QuizViewModel: function: getListQuestions
W/Firestore: (24.1.1) [CustomClassMapper]: No setter/field for Questions found on class com.hva.madlevel7task2.model.QuizBody (fields/setters are case sensitive!)
I/System.out: QuizBody(questions=null)
Stefan de Kraker
  • 333
  • 4
  • 14

1 Answers1

3

The following error:

java.lang.RuntimeException: Could not deserialize object. Class com.hva.madlevel7task2.model.QuizBody does not define a no-argument constructor.

Is very straightforward in my opinion. Your class "QuizBody" does not have a no-argument constructor. When you try to deserialize an object that comes from a Firestore database, the Android SDKs require that the class should mandatorily have a default no-argument constructor.

In Kotlin, the data classes don't provide a default no-arg constructor if all the properties of the class are declared with val. For such properties, Kotlin requires that their values be specified in the constructor since they can't possibly change later. So this is required because we need to ensure the compiler that all the properties have an initial value. You can provide to all of the properties an initial value of null or any other value you find more appropriate. So your classes should look like this:

data class QuizBody(
    var questions: List<Question>? = null
                                   
)

data class Question(
    var question: String? = null,
    var answers: List<String>? = null,
    var answer: Int? = null
)

Now adding the properties in the constructor, Kotlin will automatically generate a default no-argument constructor. In this way, the Firebase Android SDK will be able to use to generate setters for each property. If don't make this change, you won't be able to use automatic deserialization. You'll have to read the value for each property out of the DocumentSnapshot object and pass them all to Kotlin's constructor.

Edit:

if (task.isSuccessful) {
    val document = task.result
    if (document.exists()) {
        val myObject = document.toObject(QuizBody::class.java)
        println(myObject)
    }
}
Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
  • Do I still need to read the value from the `DocumentSnapshot`? I can only do `result.data` wich is all the data form the firestore. – Stefan de Kraker May 01 '22 at 09:14
  • You have to loop through the results as in my updated answer. Does it work this way? – Alex Mamo May 01 '22 at 09:24
  • I cant loop trough the `task.result` list `For-loop range must have an 'iterator()' method` And I think I need to map the document to `Question` instead of `QuizBody` because the document is a single `Question` – Stefan de Kraker May 01 '22 at 09:30
  • What do you mean, you cannot? What is going on? Are you getting any error? – Alex Mamo May 01 '22 at 09:30
  • I can do `result.data` or `result.get` but I'm not able to loop trough it. Do I also need to add `@PropertyName` like here [link](https://stackoverflow.com/a/50724843/10352461) – Stefan de Kraker May 01 '22 at 09:38
  • Oh, my bad. You reading a single document, not a query. Then please see my updated answer. Does it work now? – Alex Mamo May 01 '22 at 10:16
  • That is the same code I posted in my first post. Please see my updated post. – Stefan de Kraker May 01 '22 at 11:12
  • 1
    I see. Your array in the database is written with A (capital letter) while in your class is lowercase. [Both names must match](https://stackoverflow.com/questions/66827659/data-from-firestore-is-not-displayed-in-recyclerview). And yes, you should use `@PropertyName`. Try that and tell me if it works. – Alex Mamo May 01 '22 at 11:25
  • I got it working now, I just needed to change `questions` to `Questions`!! Thank you for your help. Do I still need to add `@PropertyName`? – Stefan de Kraker May 01 '22 at 11:42
  • 1
    Good to hear that you made it work. If you changed the name, the annotation is not needed anymore. – Alex Mamo May 01 '22 at 12:10