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 :)
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
QuestionWithAnswersanymore but you can directly return aMap<Question, List<Answer>>, see hereExample 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):
Repository Implementation (adapt interface accordingly)
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
In your composable:
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:
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.