mirror of
https://github.com/AdrianKuta/KahootQuiz.git
synced 2025-09-14 17:24:21 +02:00
feat: Implement interactive quiz screen with answer revealed state
This commit enhances the `QuizScreen` to be interactive, allowing users to select choices and view revealed answers. It also introduces HTML parsing for question text and adds new design elements like icons and colors. Key changes: - **UI Layer (`ui:quiz` module):** - `QuizScreen`: - Now takes an `onSelect` callback to handle choice selection. - `Choices` composable updated to display choices in a `LazyVerticalGrid` and handles click events. - Introduced `ChoiceItem` which branches into `ChoiceItemDefault` (for selectable choices) and `ChoiceItemRevealed` (for displaying correct/incorrect answers). - `ChoiceItemDefault` displays choices with background colors and icons based on their index. - `ChoiceItemRevealed` displays choices with background colors indicating correctness (green for correct, red for incorrect selected, pink for incorrect unselected) and appropriate icons (tick for correct, cross for wrong). - `QuestionContent` now parses HTML in the question text using `HtmlCompat` and a new `toAnnotatedString` extension. - Image loading in `QuestionContent` uses `ContentScale.FillWidth` and `heightIn(min = 200.dp)`. - Added a new preview `QuizScreenRevealedAnswerPreview` to showcase the revealed answer state. - `QuizScreenViewModel`: - Now manages `_selectedChoiceIndex` to track the user's answer. - `uiState` is now a combination of the fetched quiz and the `_selectedChoiceIndex`, producing `QuizUiState` which includes an `AnswerUiState`. - `AnswerUiState` holds the `selectedChoiceIndex`. - Implemented `onChoiceSelected(index: Int)` to update the selected choice. - **Design System (`core:designsystem` module):** - Added `TextUtils.kt` with a `Spanned.toAnnotatedString()` extension function to convert HTML formatted text (from `HtmlCompat`) into Jetpack Compose `AnnotatedString`. - Added new color definitions: `Pink`, `Red`, `Red2`, `Blue2`, `Yellow3`, `Green`, `Green2`. - Added a `contrastiveTo(color: Color)` utility function to determine a contrasting text color (black or white) for a given background color. - Added new vector drawables for choice shapes and correctness indicators: - `ic_circle.xml` - `ic_correct.xml` - `ic_diamond.xml` - `ic_square.xml` - `ic_triangle.xml` - `ic_wrong.xml` - Added Detekt configuration file (`config/detekt/detekt.yml`) for the design system module. - **Domain Layer (`domain` module):** - `Question.image` is now nullable (`String?`). - `Choice.correct` is now non-nullable (`Boolean`). - **Network Layer (`core:network` module):** - `ChoiceDto.correct` is now non-nullable (`Boolean`) to align with the domain model. - **Project Configuration:** - Added `.editorconfig` with Kotlin specific trailing comma settings. - Minor reordering of dependencies in `gradle/libs.versions.toml`.
This commit is contained in:
4
.editorconfig
Executable file
4
.editorconfig
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
[*.{kt,kts}]
|
||||||
|
ij_kotlin_allow_trailing_comma = true
|
||||||
|
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||||
|
|
33
core/designsystem/config/detekt/detekt.yml
Normal file
33
core/designsystem/config/detekt/detekt.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Exceptions for compose. See https://detekt.dev/docs/introduction/compose
|
||||||
|
naming:
|
||||||
|
FunctionNaming:
|
||||||
|
functionPattern: '[a-zA-Z][a-zA-Z0-9]*'
|
||||||
|
|
||||||
|
TopLevelPropertyNaming:
|
||||||
|
constantPattern: '[A-Z][A-Za-z0-9]*'
|
||||||
|
|
||||||
|
complexity:
|
||||||
|
LongParameterList:
|
||||||
|
ignoreAnnotated: ['Composable']
|
||||||
|
TooManyFunctions:
|
||||||
|
ignoreAnnotatedFunctions: ['Preview']
|
||||||
|
|
||||||
|
style:
|
||||||
|
MagicNumber:
|
||||||
|
ignorePropertyDeclaration: true
|
||||||
|
ignoreCompanionObjectPropertyDeclaration: true
|
||||||
|
ignoreAnnotated: ['Composable']
|
||||||
|
|
||||||
|
UnusedPrivateMember:
|
||||||
|
ignoreAnnotated: ['Composable']
|
||||||
|
|
||||||
|
# Deviations from defaults
|
||||||
|
formatting:
|
||||||
|
TrailingCommaOnCallSite:
|
||||||
|
active: true
|
||||||
|
autoCorrect: true
|
||||||
|
useTrailingCommaOnCallSite: true
|
||||||
|
TrailingCommaOnDeclarationSite:
|
||||||
|
active: true
|
||||||
|
autoCorrect: true
|
||||||
|
useTrailingCommaOnDeclarationSite: true
|
@@ -1,6 +1,15 @@
|
|||||||
package dev.adriankuta.kahootquiz.core.designsystem
|
package dev.adriankuta.kahootquiz.core.designsystem
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun contrastiveTo(color: Color): Color = if (color.luminance() < 0.5) {
|
||||||
|
Color.White
|
||||||
|
} else {
|
||||||
|
Color.Black
|
||||||
|
}
|
||||||
|
|
||||||
val Purple80 = Color(0xFFD0BCFF)
|
val Purple80 = Color(0xFFD0BCFF)
|
||||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||||
@@ -10,4 +19,11 @@ val Purple40 = Color(0xFF6650a4)
|
|||||||
val PurpleGrey40 = Color(0xFF625b71)
|
val PurpleGrey40 = Color(0xFF625b71)
|
||||||
val Pink40 = Color(0xFF7D5260)
|
val Pink40 = Color(0xFF7D5260)
|
||||||
|
|
||||||
val Grey = Color(0xFFFAFAFA)
|
val Grey = Color(0xFFFAFAFA)
|
||||||
|
val Pink = Color(0xFFFF99AA)
|
||||||
|
val Red = Color(0xFFFF3355)
|
||||||
|
val Red2 = Color(0xFFE21B3C)
|
||||||
|
val Blue2 = Color(0xFF1368CE)
|
||||||
|
val Yellow3 = Color(0xFFD89E00)
|
||||||
|
val Green = Color(0xFF66BF39)
|
||||||
|
val Green2 = Color(0xFF26890C)
|
||||||
|
@@ -0,0 +1,48 @@
|
|||||||
|
package dev.adriankuta.kahootquiz.core.designsystem
|
||||||
|
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.StyleSpan
|
||||||
|
import android.text.style.UnderlineSpan
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
|
||||||
|
fun Spanned.toAnnotatedString(): AnnotatedString = buildAnnotatedString {
|
||||||
|
val spanned = this@toAnnotatedString
|
||||||
|
append(spanned.toString())
|
||||||
|
getSpans(0, spanned.length, Any::class.java).forEach { span ->
|
||||||
|
val start = getSpanStart(span)
|
||||||
|
val end = getSpanEnd(span)
|
||||||
|
when (span) {
|
||||||
|
is StyleSpan -> when (span.style) {
|
||||||
|
Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end)
|
||||||
|
Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end)
|
||||||
|
Typeface.BOLD_ITALIC -> addStyle(
|
||||||
|
SpanStyle(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
),
|
||||||
|
start, end,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is UnderlineSpan -> addStyle(
|
||||||
|
SpanStyle(textDecoration = TextDecoration.Underline),
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
)
|
||||||
|
|
||||||
|
is ForegroundColorSpan -> addStyle(
|
||||||
|
SpanStyle(color = Color(span.foregroundColor)),
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
core/designsystem/src/main/res/drawable/ic_circle.xml
Normal file
15
core/designsystem/src/main/res/drawable/ic_circle.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="40dp"
|
||||||
|
android:height="40dp"
|
||||||
|
android:viewportWidth="40"
|
||||||
|
android:viewportHeight="40">
|
||||||
|
<path
|
||||||
|
android:pathData="M20,10C14.477,10 10,14.477 10,20C10,25.523 14.477,30 20,30C25.523,30 30,25.523 30,20C30,14.477 25.523,10 20,10Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M20,9.5C25.799,9.5 30.5,14.201 30.5,20C30.5,25.799 25.799,30.5 20,30.5C14.201,30.5 9.5,25.799 9.5,20C9.5,14.201 14.201,9.5 20,9.5Z"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
</vector>
|
24
core/designsystem/src/main/res/drawable/ic_correct.xml
Normal file
24
core/designsystem/src/main/res/drawable/ic_correct.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="40dp"
|
||||||
|
android:height="40dp"
|
||||||
|
android:viewportWidth="40"
|
||||||
|
android:viewportHeight="40">
|
||||||
|
<path
|
||||||
|
android:pathData="M20,20m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#67BF38"
|
||||||
|
android:strokeColor="#26890C"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M25.237,12.219L28.298,14.723L18.157,27.123L11.651,21.662L14.296,18.722L17.722,21.409L25.237,12.219Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M25.554,11.831L28.615,14.336L29.001,14.653L28.685,15.039L18.545,27.44L18.224,27.832L17.836,27.507L11.33,22.045L10.933,21.713L11.279,21.328L13.925,18.388L14.237,18.041L14.604,18.329L17.645,20.713L24.85,11.902L25.167,11.515L25.554,11.831Z"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M25.237,12.219L28.298,14.723L18.157,27.123L11.651,21.662L14.296,18.722L17.722,21.409L25.237,12.219ZM25.554,11.831L28.615,14.336L29.001,14.653L28.685,15.039L18.545,27.44L18.224,27.832L17.836,27.507L11.33,22.045L10.933,21.713L11.279,21.328L13.925,18.388L14.237,18.041L14.604,18.329L17.645,20.713L24.85,11.902L25.167,11.515L25.554,11.831Z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
15
core/designsystem/src/main/res/drawable/ic_diamond.xml
Normal file
15
core/designsystem/src/main/res/drawable/ic_diamond.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="40dp"
|
||||||
|
android:height="40dp"
|
||||||
|
android:viewportWidth="40"
|
||||||
|
android:viewportHeight="40">
|
||||||
|
<path
|
||||||
|
android:pathData="M20,8.75L8.75,20.004L20,31.25L31.25,20.001L20,8.75Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M20.354,8.396L31.604,19.647L31.957,20.001L31.604,20.354L20.354,31.604L20,31.957L19.646,31.604L8.396,20.357L8.043,20.004L8.396,19.65L19.646,8.396L20,8.043L20.354,8.396Z"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
</vector>
|
15
core/designsystem/src/main/res/drawable/ic_square.xml
Normal file
15
core/designsystem/src/main/res/drawable/ic_square.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="40dp"
|
||||||
|
android:height="41dp"
|
||||||
|
android:viewportWidth="40"
|
||||||
|
android:viewportHeight="41">
|
||||||
|
<path
|
||||||
|
android:pathData="M28.75,11.476H11.25V28.976H28.75V11.476Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M29.25,10.976V29.476H10.75V10.976H29.25Z"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
</vector>
|
15
core/designsystem/src/main/res/drawable/ic_triangle.xml
Normal file
15
core/designsystem/src/main/res/drawable/ic_triangle.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="40dp"
|
||||||
|
android:height="40dp"
|
||||||
|
android:viewportWidth="40"
|
||||||
|
android:viewportHeight="40">
|
||||||
|
<path
|
||||||
|
android:pathData="M20,11.25L8.75,28.75H31.25L20,11.25Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M20.421,10.979L31.671,28.479L32.166,29.25H7.834L8.329,28.479L19.579,10.979L20,10.325L20.421,10.979Z"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
</vector>
|
35
core/designsystem/src/main/res/drawable/ic_wrong.xml
Normal file
35
core/designsystem/src/main/res/drawable/ic_wrong.xml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="40dp"
|
||||||
|
android:height="40dp"
|
||||||
|
android:viewportWidth="40"
|
||||||
|
android:viewportHeight="40">
|
||||||
|
<path
|
||||||
|
android:pathData="M20,20m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
||||||
|
android:fillColor="#FF3355"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M20,20m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#FF3355"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M20,20m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
||||||
|
android:strokeAlpha="0.15"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M12.04,24.636L15.355,27.95L19.995,23.31L24.636,27.95L27.95,24.636L23.31,19.995L27.95,15.355L24.636,12.04L19.995,16.681L15.355,12.04L12.04,15.355L16.681,19.995L12.04,24.636Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M5.853,5.721h28.284v28.284h-28.284z"/>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M12.04,24.636L15.355,27.95L19.995,23.31L24.636,27.95L27.95,24.636L23.31,19.995L27.95,15.355L24.636,12.04L19.995,16.681L15.355,12.04L12.04,15.355L16.681,19.995L12.04,24.636Z"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M15.355,27.95L14.648,28.657L15.355,29.365L16.062,28.657L15.355,27.95ZM12.04,24.636L11.333,23.929L10.626,24.636L11.333,25.343L12.04,24.636ZM19.995,23.31L20.702,22.603L19.995,21.896L19.288,22.603L19.995,23.31ZM24.636,27.95L23.929,28.657L24.636,29.365L25.343,28.657L24.636,27.95ZM27.95,24.636L28.657,25.343L29.365,24.636L28.657,23.929L27.95,24.636ZM23.31,19.995L22.603,19.288L21.896,19.995L22.603,20.702L23.31,19.995ZM27.95,15.355L28.657,16.062L29.365,15.355L28.657,14.648L27.95,15.355ZM24.636,12.04L25.343,11.333L24.636,10.626L23.929,11.333L24.636,12.04ZM19.995,16.681L19.288,17.388L19.995,18.095L20.702,17.388L19.995,16.681ZM15.355,12.04L16.062,11.333L15.355,10.626L14.648,11.333L15.355,12.04ZM12.04,15.355L11.333,14.648L10.626,15.355L11.333,16.062L12.04,15.355ZM16.681,19.995L17.388,20.702L18.095,19.995L17.388,19.288L16.681,19.995ZM16.062,27.243L12.748,23.929L11.333,25.343L14.648,28.657L16.062,27.243ZM19.288,22.603L14.648,27.243L16.062,28.657L20.702,24.017L19.288,22.603ZM25.343,27.243L20.702,22.603L19.288,24.017L23.929,28.657L25.343,27.243ZM27.243,23.929L23.929,27.243L25.343,28.657L28.657,25.343L27.243,23.929ZM22.603,20.702L27.243,25.343L28.657,23.929L24.017,19.288L22.603,20.702ZM27.243,14.648L22.603,19.288L24.017,20.702L28.657,16.062L27.243,14.648ZM23.929,12.748L27.243,16.062L28.657,14.648L25.343,11.333L23.929,12.748ZM20.702,17.388L25.343,12.748L23.929,11.333L19.288,15.974L20.702,17.388ZM14.648,12.748L19.288,17.388L20.702,15.974L16.062,11.333L14.648,12.748ZM12.748,16.062L16.062,12.748L14.648,11.333L11.333,14.648L12.748,16.062ZM17.388,19.288L12.748,14.648L11.333,16.062L15.974,20.702L17.388,19.288ZM12.748,25.343L17.388,20.702L15.974,19.288L11.333,23.929L12.748,25.343Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.15"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
@@ -24,7 +24,7 @@ data class QuestionDto(
|
|||||||
|
|
||||||
data class ChoiceDto(
|
data class ChoiceDto(
|
||||||
val answer: String?,
|
val answer: String?,
|
||||||
val correct: Boolean?,
|
val correct: Boolean,
|
||||||
val languageInfo: LanguageInfoDto?
|
val languageInfo: LanguageInfoDto?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -2,6 +2,6 @@ package dev.adriankuta.kahootquiz.domain.models
|
|||||||
|
|
||||||
data class Choice(
|
data class Choice(
|
||||||
val answer: String?,
|
val answer: String?,
|
||||||
val correct: Boolean?,
|
val correct: Boolean,
|
||||||
val languageInfo: LanguageInfo? = null
|
val languageInfo: LanguageInfo? = null
|
||||||
)
|
)
|
@@ -12,12 +12,12 @@ data class Question(
|
|||||||
val pointsMultiplier: Int?,
|
val pointsMultiplier: Int?,
|
||||||
val choices: List<Choice>?,
|
val choices: List<Choice>?,
|
||||||
val layout: String? = null,
|
val layout: String? = null,
|
||||||
val image: String,
|
val image: String? = null,
|
||||||
val imageMetadata: ImageMetadata?,
|
val imageMetadata: ImageMetadata?,
|
||||||
val resources: String? = null,
|
val resources: String? = null,
|
||||||
val video: Video? = null,
|
val video: Video? = null,
|
||||||
val questionFormat: Int? = null,
|
val questionFormat: Int? = null,
|
||||||
val languageInfo: LanguageInfo? = null,
|
val languageInfo: LanguageInfo? = null,
|
||||||
val media: List<MediaItem>? = null,
|
val media: List<MediaItem>? = null,
|
||||||
val choiceRange: ChoiceRange? = null
|
val choiceRange: ChoiceRange? = null,
|
||||||
)
|
)
|
@@ -1,7 +1,4 @@
|
|||||||
[versions]
|
[versions]
|
||||||
coilCompose = "3.3.0"
|
|
||||||
coilNetworkOkhttp = "3.3.0"
|
|
||||||
retrofit = "3.0.0"
|
|
||||||
targetSdk = "36"
|
targetSdk = "36"
|
||||||
compileSdk = "36"
|
compileSdk = "36"
|
||||||
minSdk = "23"
|
minSdk = "23"
|
||||||
@@ -23,6 +20,8 @@ animation = "1.9.0"
|
|||||||
appUpdateKtx = "2.1.0"
|
appUpdateKtx = "2.1.0"
|
||||||
appcompat = "1.7.1"
|
appcompat = "1.7.1"
|
||||||
billing = "8.0.0"
|
billing = "8.0.0"
|
||||||
|
coilCompose = "3.3.0"
|
||||||
|
coilNetworkOkhttp = "3.3.0"
|
||||||
coreTest = "1.7.0" # https://developer.android.com/jetpack/androidx/releases/test
|
coreTest = "1.7.0" # https://developer.android.com/jetpack/androidx/releases/test
|
||||||
datastorePreferences = "1.1.7" # https://developer.android.com/topic/libraries/architecture/datastore#preferences-datastore-dependencies
|
datastorePreferences = "1.1.7" # https://developer.android.com/topic/libraries/architecture/datastore#preferences-datastore-dependencies
|
||||||
datetime = "0.7.1" # https://github.com/Kotlin/kotlinx-datetime/releases
|
datetime = "0.7.1" # https://github.com/Kotlin/kotlinx-datetime/releases
|
||||||
@@ -48,6 +47,7 @@ material = "1.12.0"
|
|||||||
materialIconsExtended = "1.7.8"
|
materialIconsExtended = "1.7.8"
|
||||||
mockk = "1.14.5" # https://github.com/mockk/mockk/releases
|
mockk = "1.14.5" # https://github.com/mockk/mockk/releases
|
||||||
playServicesAds = "24.5.0"
|
playServicesAds = "24.5.0"
|
||||||
|
retrofit = "3.0.0"
|
||||||
reviewKtx = "2.0.2"
|
reviewKtx = "2.0.2"
|
||||||
room = "2.7.2"
|
room = "2.7.2"
|
||||||
secrets = "2.0.1"
|
secrets = "2.0.1"
|
||||||
|
@@ -2,17 +2,24 @@ package dev.adriankuta.kahootquiz.ui.quiz
|
|||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -26,11 +33,21 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import dev.adriankuta.kahootquiz.core.designsystem.Blue2
|
||||||
|
import dev.adriankuta.kahootquiz.core.designsystem.Green
|
||||||
|
import dev.adriankuta.kahootquiz.core.designsystem.Green2
|
||||||
import dev.adriankuta.kahootquiz.core.designsystem.Grey
|
import dev.adriankuta.kahootquiz.core.designsystem.Grey
|
||||||
import dev.adriankuta.kahootquiz.core.designsystem.KahootQuizTheme
|
import dev.adriankuta.kahootquiz.core.designsystem.KahootQuizTheme
|
||||||
|
import dev.adriankuta.kahootquiz.core.designsystem.Pink
|
||||||
|
import dev.adriankuta.kahootquiz.core.designsystem.Red
|
||||||
|
import dev.adriankuta.kahootquiz.core.designsystem.Red2
|
||||||
|
import dev.adriankuta.kahootquiz.core.designsystem.Yellow3
|
||||||
|
import dev.adriankuta.kahootquiz.core.designsystem.contrastiveTo
|
||||||
|
import dev.adriankuta.kahootquiz.core.designsystem.toAnnotatedString
|
||||||
import dev.adriankuta.kahootquiz.domain.models.Choice
|
import dev.adriankuta.kahootquiz.domain.models.Choice
|
||||||
import dev.adriankuta.kahootquiz.domain.models.Question
|
import dev.adriankuta.kahootquiz.domain.models.Question
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
@@ -39,20 +56,22 @@ import dev.adriankuta.kahootquiz.core.designsystem.R as DesignR
|
|||||||
@Composable
|
@Composable
|
||||||
fun QuizScreen(
|
fun QuizScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: QuizScreenViewModel = hiltViewModel()
|
viewModel: QuizScreenViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
QuizScreen(
|
QuizScreen(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
modifier = modifier.fillMaxSize()
|
onSelect = viewModel::onChoiceSelected,
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun QuizScreen(
|
private fun QuizScreen(
|
||||||
uiState: QuizUiState,
|
uiState: QuizUiState,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(modifier.fillMaxSize()) {
|
Box(modifier.fillMaxSize()) {
|
||||||
@@ -60,23 +79,27 @@ private fun QuizScreen(
|
|||||||
painter = painterResource(id = DesignR.drawable.bg_image),
|
painter = painterResource(id = DesignR.drawable.bg_image),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Toolbar(
|
Toolbar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(72.dp)
|
.height(72.dp)
|
||||||
.padding(8.dp)
|
.padding(8.dp),
|
||||||
)
|
)
|
||||||
QuestionContent(
|
QuestionContent(
|
||||||
question = uiState.currentQuestion ?: return,
|
question = uiState.currentQuestion ?: return,
|
||||||
modifier = Modifier.padding(horizontal = 8.dp)
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
)
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
Choices(
|
Choices(
|
||||||
choices = uiState.currentQuestion.choices ?: emptyList() // TODO remove empty list
|
choices = uiState.currentQuestion.choices ?: emptyList(), // TODO remove empty list
|
||||||
|
answer = uiState.answer,
|
||||||
|
onSelect = onSelect,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,10 +107,10 @@ private fun QuizScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Toolbar(
|
private fun Toolbar(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "2/24",
|
text = "2/24",
|
||||||
@@ -95,9 +118,9 @@ private fun Toolbar(
|
|||||||
.align(Alignment.CenterStart)
|
.align(Alignment.CenterStart)
|
||||||
.background(
|
.background(
|
||||||
color = Grey,
|
color = Grey,
|
||||||
shape = RoundedCornerShape(60.dp)
|
shape = RoundedCornerShape(60.dp),
|
||||||
)
|
)
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -105,14 +128,14 @@ private fun Toolbar(
|
|||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
.background(
|
.background(
|
||||||
color = Grey,
|
color = Grey,
|
||||||
shape = RoundedCornerShape(60.dp)
|
shape = RoundedCornerShape(60.dp),
|
||||||
)
|
)
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = DesignR.drawable.ic_type),
|
painter = painterResource(id = DesignR.drawable.ic_type),
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -128,38 +151,175 @@ private fun QuestionContent(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = question.image,
|
model = question.image,
|
||||||
contentDescription = question.imageMetadata?.altText,
|
contentDescription = question.imageMetadata?.altText,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.FillWidth,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(200.dp)
|
.heightIn(min = 200.dp),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = question.question ?: "",
|
text = HtmlCompat.fromHtml(
|
||||||
|
question.question ?: "",
|
||||||
|
HtmlCompat.FROM_HTML_MODE_COMPACT,
|
||||||
|
).toAnnotatedString(),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(
|
.background(
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
shape = RoundedCornerShape(4.dp)
|
shape = RoundedCornerShape(4.dp),
|
||||||
)
|
)
|
||||||
.padding(horizontal = 8.dp, vertical = 16.dp)
|
.padding(horizontal = 8.dp, vertical = 16.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Choices(
|
private fun Choices(
|
||||||
choices: List<Choice>
|
choices: List<Choice>,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
|
answer: AnswerUiState?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LazyVerticalGrid() { }
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(2),
|
||||||
|
contentPadding = PaddingValues(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
itemsIndexed(choices) { index, choice ->
|
||||||
|
ChoiceItem(
|
||||||
|
choice = choice,
|
||||||
|
index = index,
|
||||||
|
answer = answer,
|
||||||
|
onClick = { onSelect(index) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChoiceItem(
|
||||||
|
choice: Choice,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
index: Int,
|
||||||
|
answer: AnswerUiState?,
|
||||||
|
) {
|
||||||
|
if (answer != null) {
|
||||||
|
ChoiceItemRevealed(
|
||||||
|
choice = choice,
|
||||||
|
index = index,
|
||||||
|
isSelected = answer.selectedChoiceIndex == index,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ChoiceItemDefault(
|
||||||
|
choice = choice,
|
||||||
|
index = index,
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChoiceItemDefault(
|
||||||
|
choice: Choice,
|
||||||
|
index: Int,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val backgroundColor = when (index) {
|
||||||
|
0 -> Red2
|
||||||
|
1 -> Blue2
|
||||||
|
2 -> Yellow3
|
||||||
|
3 -> Green2
|
||||||
|
else -> Color.Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Add icons
|
||||||
|
val icon = when (index) {
|
||||||
|
0 -> DesignR.drawable.ic_triangle
|
||||||
|
1 -> DesignR.drawable.ic_diamond
|
||||||
|
2 -> DesignR.drawable.ic_circle
|
||||||
|
else -> DesignR.drawable.ic_square
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(backgroundColor, shape = RoundedCornerShape(4.dp))
|
||||||
|
.height(100.dp)
|
||||||
|
.clickable(
|
||||||
|
onClick = onClick,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.size(32.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = choice.answer ?: "",
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
color = contrastiveTo(backgroundColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChoiceItemRevealed(
|
||||||
|
choice: Choice,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
) {
|
||||||
|
val backgroundColor = when {
|
||||||
|
isSelected && !choice.correct -> Red
|
||||||
|
choice.correct -> Green
|
||||||
|
else -> Pink
|
||||||
|
}
|
||||||
|
|
||||||
|
val icon = if (choice.correct) {
|
||||||
|
DesignR.drawable.ic_correct
|
||||||
|
} else {
|
||||||
|
DesignR.drawable.ic_wrong
|
||||||
|
}
|
||||||
|
|
||||||
|
val alignment = if (index % 2 == 0) {
|
||||||
|
Alignment.TopStart
|
||||||
|
} else {
|
||||||
|
Alignment.TopEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(backgroundColor, shape = RoundedCornerShape(4.dp))
|
||||||
|
.height(100.dp),
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(alignment)
|
||||||
|
.offset(
|
||||||
|
x = if (alignment == Alignment.TopStart) (-8).dp else (8).dp,
|
||||||
|
(-8).dp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = choice.answer ?: "",
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
color = contrastiveTo(backgroundColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun QuizScreenPreview() {
|
private fun QuizScreenPreview() {
|
||||||
@@ -172,7 +332,7 @@ private fun QuizScreenPreview() {
|
|||||||
Choice(answer = "Berlin", correct = false),
|
Choice(answer = "Berlin", correct = false),
|
||||||
Choice(answer = "Madrid", correct = false),
|
Choice(answer = "Madrid", correct = false),
|
||||||
Choice(answer = "Paris", correct = true),
|
Choice(answer = "Paris", correct = true),
|
||||||
Choice(answer = "Rome", correct = false)
|
Choice(answer = "Rome", correct = false),
|
||||||
),
|
),
|
||||||
pointsMultiplier = 1,
|
pointsMultiplier = 1,
|
||||||
time = 30.seconds,
|
time = 30.seconds,
|
||||||
@@ -180,7 +340,41 @@ private fun QuizScreenPreview() {
|
|||||||
imageMetadata = null,
|
imageMetadata = null,
|
||||||
)
|
)
|
||||||
QuizScreen(
|
QuizScreen(
|
||||||
uiState = QuizUiState(currentQuestion = sampleQuestion)
|
uiState = QuizUiState(
|
||||||
|
currentQuestion = sampleQuestion,
|
||||||
|
),
|
||||||
|
onSelect = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun QuizScreenRevealedAnswerPreview() {
|
||||||
|
KahootQuizTheme {
|
||||||
|
val sampleQuestion = Question(
|
||||||
|
type = "quiz",
|
||||||
|
image = "", // Add a sample image URL or leave empty
|
||||||
|
question = "What is the capital of France?",
|
||||||
|
choices = listOf(
|
||||||
|
Choice(answer = "Berlin", correct = false),
|
||||||
|
Choice(answer = "Madrid", correct = false),
|
||||||
|
Choice(answer = "Paris", correct = true),
|
||||||
|
Choice(answer = "Rome", correct = false),
|
||||||
|
),
|
||||||
|
pointsMultiplier = 1,
|
||||||
|
time = 30.seconds,
|
||||||
|
questionFormat = 0,
|
||||||
|
imageMetadata = null,
|
||||||
|
)
|
||||||
|
QuizScreen(
|
||||||
|
uiState = QuizUiState(
|
||||||
|
currentQuestion = sampleQuestion,
|
||||||
|
answer = AnswerUiState(
|
||||||
|
selectedChoiceIndex = 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onSelect = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,26 +6,42 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import dev.adriankuta.kahootquiz.domain.models.Question
|
import dev.adriankuta.kahootquiz.domain.models.Question
|
||||||
import dev.adriankuta.kahootquiz.domain.usecases.GetQuizUseCase
|
import dev.adriankuta.kahootquiz.domain.usecases.GetQuizUseCase
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class QuizScreenViewModel @Inject constructor(
|
class QuizScreenViewModel @Inject constructor(
|
||||||
private val getQuizUseCase: GetQuizUseCase
|
private val getQuizUseCase: GetQuizUseCase,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _uiState = MutableStateFlow(QuizUiState())
|
private val _selectedChoiceIndex = MutableStateFlow<Int?>(null)
|
||||||
val uiState: StateFlow<QuizUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<QuizUiState> = combine(
|
||||||
|
suspend { getQuizUseCase() }.asFlow(),
|
||||||
|
_selectedChoiceIndex,
|
||||||
|
) { quiz, selectedChoiceIndex ->
|
||||||
|
QuizUiState(
|
||||||
|
currentQuestion = quiz.questions.first(),
|
||||||
|
answer = selectedChoiceIndex?.let { AnswerUiState(it) },
|
||||||
|
)
|
||||||
|
}.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
|
initialValue = QuizUiState(),
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
fun onChoiceSelected(index: Int) {
|
||||||
viewModelScope.launch {
|
_selectedChoiceIndex.value = index
|
||||||
_uiState.value = QuizUiState(getQuizUseCase().questions.first())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class QuizUiState(
|
data class QuizUiState(
|
||||||
val currentQuestion: Question? = null
|
val currentQuestion: Question? = null,
|
||||||
)
|
val answer: AnswerUiState? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AnswerUiState(
|
||||||
|
val selectedChoiceIndex: Int,
|
||||||
|
)
|
||||||
|
Reference in New Issue
Block a user