1

Disclaimer: I'm a complete beginner to Android development in Kotlin and just started some weeks ago.

My goal is to create an Quiz App which gets it's Questions and Answers from a local, prepopulated Room database.


SQLITE:

The sqlite database has the following schema:
sqlite> .schema
CREATE TABLE quiz_questions(ID INTEGER PRIMARY KEY, question TEXT, quest_result INTEGER NOT NULL DEFAULT 0);
CREATE TABLE quiz_answers(ID INTEGER PRIMARY KEY, quest_id INTEGER, answer TEXT, is_true INTEGER NOT NULL);

=> The quest_id is the foreign key which reflects the ID of the related question
=> One question has many answers (specifically 4 in my case)


Links:

I used the following guides to create my app and add the relationship:
General App: https://www.youtube.com/watch?v=8YPXv7xKh2w
Relationship: https://developer.android.com/training/data-storage/room/relationships#one-to-many


Entities:

Question.kt:

@Entity(tableName = "quiz_questions")
data class Question(
    @PrimaryKey val ID: Int,
    val question: String?,
    var quest_result: Boolean
)

Answer.kt:

@Entity(tableName = "quiz_answers")
data class Answer(
    @PrimaryKey val ID: Int,
    @ColumnInfo(index = true) val quest_id: Int,
    val answer: String,
    val is_true: Boolean
)

QuestionWithAnswers.kt:

data class QuestionWithAnswers(
    @Embedded val question: Question,
    @Relation(
        entity = Answer::class,
        parentColumn = "ID",
        entityColumn = "quest_id"
    )
    val answer: List<Answer>
)

Repository:

interface Rep_QuestionWithAnswers {
    fun getQuestionAndAnswers(id: Int): List<QuestionWithAnswers>
}

UseCase:

class UC_GetQuestionWithAnswers (
    private val repository: Rep_QuestionWithAnswers
        ) {
    operator fun invoke(id: Int): List<QuestionWithAnswers> {
        return repository.getQuestionAndAnswers(id)
    }
}

Implementation:

class Impl_Rep_QuestionWithAnswers (
    private val dao: Dao_QuestionWithAnswers
        ) : Rep_QuestionWithAnswers {
    override fun getQuestionAndAnswers(id: Int): List<QuestionWithAnswers> {
        return dao.getQuestionAndAnswers(id)
    }
}

Dao:

@Dao
interface Dao_QuestionWithAnswers {
    @Transaction
    @Query("SELECT * FROM quiz_questions WHERE ID = :id")
    fun getQuestionAndAnswers(id: Int): List<QuestionWithAnswers>
}

QuizDatabase:

@Database(
    entities = [
            Question::class,
            Answer::class,
            QuestionWithAnswers::class
            ],
    version = 1
)
abstract class QuizDatabase : RoomDatabase () {
    abstract val Dao_Question : Dao_Question
    abstract val Dao_Answer : Dao_Answer
    abstract val Dao_QuestionWithAnswers : Dao_QuestionWithAnswers

    companion object {
        const val DATABASE_NAME = "quiz_db"
    }
}

=> I created the Rep_, Impl_ and Dao_ for Answer and Question as well but didn't upload them here


Expectations:

  • I was expecting it to compile first of all
  • I want to get all questions and answers so that I can put them in a for loop and show them on the screen
  • When a answer is chosen I need to check whether the ID of the answer has value is_true = true
  • When a question was answered correctly, I need to update the result to value = true of the question with ID

What I already tried:

  • Define the foreign key in the Answer.kt file without changing anything else
@Entity(
    tableName = "quiz_answers",
    foreignKeys = [
        ForeignKey(
            entity = Question::class,
            parentColumns = ["ID"],
            childColumns = ["quest_id"]
        )
    ]
)
data class Answer(
    @PrimaryKey val ID: Int,
    @ColumnInfo(index = true) val quest_id: Int,
    val answer: String,
    val is_true: Boolean
)


Errors:

  • error: Entity class must be annotated with @Entity
  • error: Entities cannot have relations
  • error: An entity must have at least 1 field annotated with @PrimaryKey

Questions:

  • Is it correct to create an individual Dao_, Rep_ and Impl_ file for the relationship?
  • Why does the compiler assume that the data class QuestionWithAnswers should be an entity?
  • Is collection the correct datatype for the result of the query?

Thank's for all answers :)

Duffischer
  • 11
  • 1

1 Answers1

0

Multiple Daos/Repositories

There is no strict rule (see discussion here, I would do one repository, and maybe split the Daos into one for Question and one for answers (none for Question with answers, see below)

Method signature

Your methods in the dao (and consequently in the repository) must be suspension functions, as the database needs time to fetch the elements. Alternatively you can return a Flow, in case you want a stream of updates. In your code, you pass an ID as parameter, but still return a list - this seems weird. Either you return a single element by ID, or you return a list. I will assume the second one.

Retrieving Relations

Since Room 2.4 you don't need to create classes like QuestionWithAnswers anymore but you can directly return a Map<Question, List<Answer>>, see here

Example Code

This is how it would roughly look:

Dao (side note: you may want to rename your tables to be singular to make the queries more natural to read, but that is up to personal preference):

@Dao
interface Dao_QuestionWithAnswers {
    // no need for @Transaction as this is a single query
    @Query("SELECT * FROM quiz_questions JOIN quiz_answers ON quiz_questions.id = quiz_answers.quest_id")
    suspend fun getQuestionAndAnswers(): Map<Question, List<Answer>>

    // alternative signature (same @Query) - use this if you want to automatically update your UI if the questions/answers change
    // Note that is not a suspend fun, because a flow is a "wrapper" object - you will need a suspend fun when you collect it
    // This is what you normally want, so I will continue with the flow version
    fun getQuestionAndAnswers(): Flow<Map<Question, List<Answer>>>
}

Repository Implementation (adapt interface accordingly)

class Impl_Rep_QuestionWithAnswers (
    private val dao: Dao_QuestionWithAnswers
) : Rep_QuestionWithAnswers {
    override fun getQuestionAndAnswers(): Flow<Map<Question, List<Answer>>> = dao.getQuestionAndAnswers()
}

Use Case: Don't use them for now - I have the feeling it is overkill for you at the moment and just confusing. Use cases make sense when your ViewModels get too complex

ViewModel

class QuizViewModel(
    private val repository: Rep_QuestionWithAnswers
) : ViewModel() {
    val uiStateFlow = repository.getQuestionAndAnswers()
       .map {
          // turn your data into a UI class here
       }
}

In your composable:

@Composable
fun MainScreen(viewModel: QuizViewModel) {
   // no need for launching any coroutines - `collectAsState` handles
   // turning the flow into a state for you
   val uiState = viewModel.uiStateFlow.collectAsStateWithLifecycle()
}

If you have some other suspend functions in your repository that are not flows (e.g. if you want to update your DB), you should launch a coroutine that calls these in your viewModelScope, not in a global scope, like this:

// inside your viewmodel class:
fun callSomeRepositorySuspendFun() {
    viewModelScope.launch {
        repository.someSuspendFun()
    }
}

General advice: Try to start simple and don't overcomplicate things by trying to use every pattern (clean architecture, etc.) at once, instead start as simple as possible and introduce more patterns once the need for them is there. Also, use the official android documentation, it is really helpful.

Robin
  • 333
  • 1
  • 12
  • I now modified my project to use just Rep_Quiz and changed all functions in the two Dao's to suspend functions. Now I need to execute these with coroutine. I tried the following: `class UC_GetQuestionWithAnswers ( private val repository: Rep_Quiz ) { operator fun invoke(id: Int): Map> { GlobalScope.launch (Dispatchers.IO) { val questionwithanswers = async { repository.getQuestionWithAnswers(id) } return questionwithanswers.await() } } }` – Duffischer Aug 13 '23 at 15:26
  • I included a snipped how to call repository suspend functions in the ViewModel. But I think you are better off using a Flow, so I included this in my answer, too. Generally, never use the GlobalScope, launch application logic suspension functions in the viewModelScope, and ui suspension functions in a `LaunchedEffect`, `remememberCoroutineScope`, or similar (there are loads of posts explaining the difference of these) – Robin Aug 14 '23 at 09:19