mirror of
				https://github.com/AdrianKuta/KahootQuiz.git
				synced 2025-10-31 08:53:42 +01: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) | ||||||
| @@ -11,3 +20,10 @@ 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