feat: Enhance QuizScreen UI and introduce core design system module

This commit significantly revamps the `QuizScreen` UI to display question details including image and text, and introduces a new `core:designsystem` module to centralize theme, colors, typography, and drawable resources.

Key changes:

- **UI Layer (`ui:quiz` module):**
    - Updated `QuizScreen.kt`:
        - Implemented a background image for the screen.
        - Added a `Toolbar` composable to display question progress and type.
        - Created `QuestionContent` composable to show the question image (using Coil for image loading) and text.
        - Added a placeholder `Choices` composable (currently an empty `LazyVerticalGrid`).
        - Applied `fillMaxSize()` to the main `QuizScreen` modifier.
        - Included a `@Preview` for `QuizScreen` with sample data.
    - Modified `QuizScreenViewModel.kt` to update `QuizUiState` with the first `Question` from the fetched quiz.
    - Added a `quiz` string resource in `strings.xml`.
    - Added dependencies for Coil (compose and okhttp) in `ui/quiz/build.gradle.kts`.
- **Core Design System (`core:designsystem` module):**
    - Created a new Android library module `core:designsystem`.
    - Moved `Color.kt`, `Theme.kt`, and `Type.kt` from `app/src/main/java/dev/adriankuta/kahootquiz/ui/theme` to this new module.
    - Added a new `Grey` color to `Color.kt`.
    - Added `bg_image.webp` and `ic_type.xml` drawable resources.
    - Configured the module with `kahootquiz.android.library.compose` plugin.
- **App Module (`app` module):**
    - Updated `MainActivity.kt` to import `KahootQuizTheme` from the new `core.designsystem` package.
    - Added `implementation(projects.core.designsystem)` dependency in `app/build.gradle.kts`.
- **Domain Layer (`domain` module):**
    - Made several fields in `Question.kt` and `Choice.kt` nullable and provided default null values to accommodate potential missing data from the API.
    - Specifically, `Question.image` is now non-nullable (`String`).
- **Build System:**
    - Added `coilCompose` and `coilNetworkOkhttp` versions to `gradle/libs.versions.toml`.
    - Included `:core:designsystem` in `settings.gradle.kts`.
This commit is contained in:
2025-09-03 21:57:18 +02:00
parent 57313de1d7
commit 45550ecf76
17 changed files with 249 additions and 24 deletions

1
.idea/gradle.xml generated
View File

@@ -26,6 +26,7 @@
<option value="$PROJECT_DIR$/build-logic" /> <option value="$PROJECT_DIR$/build-logic" />
<option value="$PROJECT_DIR$/build-logic/convention" /> <option value="$PROJECT_DIR$/build-logic/convention" />
<option value="$PROJECT_DIR$/core" /> <option value="$PROJECT_DIR$/core" />
<option value="$PROJECT_DIR$/core/designsystem" />
<option value="$PROJECT_DIR$/core/network" /> <option value="$PROJECT_DIR$/core/network" />
<option value="$PROJECT_DIR$/domain" /> <option value="$PROJECT_DIR$/domain" />
<option value="$PROJECT_DIR$/model" /> <option value="$PROJECT_DIR$/model" />

View File

@@ -27,10 +27,10 @@ android {
} }
dependencies { dependencies {
implementation(projects.ui.quiz) implementation(projects.core.designsystem)
implementation(projects.domain) implementation(projects.domain)
implementation(projects.model.data) implementation(projects.model.data)
implementation(projects.ui.quiz)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)

View File

@@ -5,7 +5,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.adriankuta.kahootquiz.ui.theme.KahootQuizTheme import dev.adriankuta.kahootquiz.core.designsystem.KahootQuizTheme
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {

View File

@@ -0,0 +1,7 @@
plugins {
alias(libs.plugins.kahootquiz.android.library.compose)
}
android {
namespace = "dev.adriankuta.kahootquiz.core.designsystem"
}

View File

@@ -1,4 +1,4 @@
package dev.adriankuta.kahootquiz.ui.theme package dev.adriankuta.kahootquiz.core.designsystem
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -8,4 +8,6 @@ val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4) val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71) val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260) val Pink40 = Color(0xFF7D5260)
val Grey = Color(0xFFFAFAFA)

View File

@@ -1,6 +1,5 @@
package dev.adriankuta.kahootquiz.ui.theme package dev.adriankuta.kahootquiz.core.designsystem
import android.app.Activity
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme

View File

@@ -1,4 +1,4 @@
package dev.adriankuta.kahootquiz.ui.theme package dev.adriankuta.kahootquiz.core.designsystem
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,50 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,22C6.477,22 2,17.523 2,12C2,6.477 6.477,2 12,2C17.523,2 22,6.477 22,12C22,17.523 17.523,22 12,22Z"
android:fillColor="#F2F2F2"
android:fillType="evenOdd"/>
<path
android:pathData="M19.802,21.989L7.899,23.992C7.584,24.045 7.285,23.833 7.232,23.517L3.724,2.678C3.671,2.363 3.883,2.064 4.198,2.011L16.101,0.008C16.416,-0.045 16.715,0.168 16.768,0.483L20.276,21.322C20.33,21.637 20.117,21.936 19.802,21.989Z"
android:fillColor="#333333"
android:fillType="evenOdd"/>
<path
android:pathData="M19.083,20.611L8.127,22.455C7.951,22.485 7.784,22.366 7.754,22.189L4.538,3.09C4.509,2.913 4.627,2.746 4.804,2.716L15.759,0.872C15.936,0.843 16.103,0.962 16.133,1.139L19.349,20.238C19.378,20.414 19.259,20.582 19.083,20.611Z"
android:fillColor="#FAFAFA"
android:fillType="evenOdd"/>
<path
android:pathData="M18.499,20.05L13.849,20.833C13.79,20.843 13.733,20.803 13.724,20.744L12.306,12.325C12.296,12.266 12.336,12.209 12.395,12.199L17.045,11.417C17.104,11.407 17.16,11.447 17.17,11.506L18.587,19.925C18.597,19.984 18.558,20.04 18.499,20.05Z"
android:fillColor="#26890C"
android:fillType="evenOdd"/>
<path
android:pathData="M12.967,20.981L8.317,21.764C8.258,21.774 8.202,21.733 8.192,21.674L6.775,13.255C6.765,13.196 6.805,13.14 6.864,13.13L11.514,12.347C11.573,12.337 11.629,12.377 11.639,12.437L13.057,20.856C13.066,20.915 13.026,20.971 12.967,20.981Z"
android:fillColor="#FFA602"
android:fillType="evenOdd"/>
<path
android:pathData="M16.947,10.835L12.297,11.618C12.238,11.628 12.182,11.588 12.172,11.529L10.64,2.434C10.631,2.374 10.67,2.318 10.73,2.308L15.379,1.526C15.439,1.516 15.495,1.556 15.505,1.615L17.036,10.71C17.046,10.769 17.006,10.825 16.947,10.835Z"
android:fillColor="#1368CE"
android:fillType="evenOdd"/>
<path
android:pathData="M11.416,11.766L6.766,12.548C6.707,12.558 6.651,12.518 6.641,12.459L5.109,3.364C5.099,3.305 5.139,3.249 5.199,3.239L9.848,2.456C9.907,2.446 9.964,2.486 9.974,2.546L11.505,11.641C11.515,11.7 11.475,11.756 11.416,11.766Z"
android:fillColor="#E11C3C"
android:fillType="evenOdd"/>
<path
android:pathData="M7.317,8.677L8.107,6.504L9.544,8.316L7.317,8.677Z"
android:fillColor="#FAFAFA"
android:fillType="evenOdd"/>
<path
android:pathData="M13.596,5.437L15.014,6.416L14.017,7.822L12.598,6.842L13.596,5.437Z"
android:fillColor="#FAFAFA"
android:fillType="evenOdd"/>
<path
android:pathData="M10.79,16.727C10.704,16.175 10.188,15.796 9.637,15.881C9.086,15.967 8.709,16.484 8.795,17.037C8.881,17.59 9.397,17.968 9.948,17.883C10.498,17.798 10.875,17.28 10.79,16.727Z"
android:fillColor="#FAFAFA"
android:fillType="evenOdd"/>
<path
android:pathData="M16.618,16.843L14.628,17.181L14.288,15.184L16.278,14.846L16.618,16.843Z"
android:fillColor="#FAFAFA"
android:fillType="evenOdd"/>
</vector>

View File

@@ -3,5 +3,5 @@ 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? val languageInfo: LanguageInfo? = null
) )

View File

@@ -8,16 +8,16 @@ data class Question(
val type: String?, val type: String?,
val question: String?, val question: String?,
val time: Duration?, val time: Duration?,
val points: Boolean?, val points: Boolean? = null,
val pointsMultiplier: Int?, val pointsMultiplier: Int?,
val choices: List<Choice>?, val choices: List<Choice>?,
val layout: String?, val layout: String? = null,
val image: String?, val image: String,
val imageMetadata: ImageMetadata?, val imageMetadata: ImageMetadata?,
val resources: String?, val resources: String? = null,
val video: Video?, val video: Video? = null,
val questionFormat: Int?, val questionFormat: Int? = null,
val languageInfo: LanguageInfo?, val languageInfo: LanguageInfo? = null,
val media: List<MediaItem>?, val media: List<MediaItem>? = null,
val choiceRange: ChoiceRange? val choiceRange: ChoiceRange? = null
) )

View File

@@ -1,4 +1,6 @@
[versions] [versions]
coilCompose = "3.3.0"
coilNetworkOkhttp = "3.3.0"
retrofit = "3.0.0" retrofit = "3.0.0"
targetSdk = "36" targetSdk = "36"
compileSdk = "36" compileSdk = "36"
@@ -94,6 +96,8 @@ androidx-work-testing = { group = "androidx.work", name = "work-testing", versio
app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdateKtx" } app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdateKtx" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
billing-ktx = { group = "com.android.billingclient", name = "billing-ktx", version.ref = "billing" } billing-ktx = { group = "com.android.billingclient", name = "billing-ktx", version.ref = "billing" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilNetworkOkhttp" }
detekt-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" } detekt-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" }
detekt-ktlint = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detekt-ktlint = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
dotlottie-android = { module = "com.github.LottieFiles:dotlottie-android", version.ref = "dotlottieAndroid" } dotlottie-android = { module = "com.github.LottieFiles:dotlottie-android", version.ref = "dotlottieAndroid" }

View File

@@ -25,6 +25,7 @@ rootProject.name = "KahootQuiz"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
include(":app") include(":app")
include(":core:designsystem")
include(":core:network") include(":core:network")
include(":domain") include(":domain")
include(":model:data") include(":model:data")

View File

@@ -9,7 +9,11 @@ android {
} }
dependencies { dependencies {
implementation(projects.core.designsystem)
implementation(projects.domain) implementation(projects.domain)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.timber) implementation(libs.timber)
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
} }

View File

@@ -1,12 +1,40 @@
package dev.adriankuta.kahootquiz.ui.quiz package dev.adriankuta.kahootquiz.ui.quiz
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
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
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
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 dev.adriankuta.kahootquiz.core.designsystem.Grey
import dev.adriankuta.kahootquiz.core.designsystem.KahootQuizTheme
import dev.adriankuta.kahootquiz.domain.models.Choice
import dev.adriankuta.kahootquiz.domain.models.Question
import kotlin.time.Duration.Companion.seconds
import dev.adriankuta.kahootquiz.core.designsystem.R as DesignR
@Composable @Composable
fun QuizScreen( fun QuizScreen(
@@ -18,7 +46,7 @@ fun QuizScreen(
QuizScreen( QuizScreen(
uiState = uiState, uiState = uiState,
modifier = modifier modifier = modifier.fillMaxSize()
) )
} }
@@ -27,7 +55,132 @@ private fun QuizScreen(
uiState: QuizUiState, uiState: QuizUiState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(modifier) { Box(modifier.fillMaxSize()) {
Text(uiState.quiz?.id?.value ?: "") Image(
painter = painterResource(id = DesignR.drawable.bg_image),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
Column(
modifier = Modifier.fillMaxSize()
) {
Toolbar(
modifier = Modifier
.fillMaxWidth()
.height(72.dp)
.padding(8.dp)
)
QuestionContent(
question = uiState.currentQuestion ?: return,
modifier = Modifier.padding(horizontal = 8.dp)
)
Choices(
choices = uiState.currentQuestion.choices ?: emptyList() // TODO remove empty list
)
}
}
}
@Composable
private fun Toolbar(
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
) {
Text(
text = "2/24",
modifier = Modifier
.align(Alignment.CenterStart)
.background(
color = Grey,
shape = RoundedCornerShape(60.dp)
)
.padding(horizontal = 8.dp, vertical = 4.dp)
)
Row(
modifier = Modifier
.align(Alignment.Center)
.background(
color = Grey,
shape = RoundedCornerShape(60.dp)
)
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Image(
painter = painterResource(id = DesignR.drawable.ic_type),
contentDescription = "",
modifier = Modifier.size(24.dp)
)
Spacer(Modifier.width(4.dp))
Text(
text = stringResource(R.string.quiz),
)
}
}
}
@Composable
private fun QuestionContent(
question: Question,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
) {
AsyncImage(
model = question.image,
contentDescription = question.imageMetadata?.altText,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
)
Spacer(Modifier.height(16.dp))
Text(
text = question.question ?: "",
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.background(
color = Color.White,
shape = RoundedCornerShape(4.dp)
)
.padding(horizontal = 8.dp, vertical = 16.dp)
)
}
}
@Composable
private fun Choices(
choices: List<Choice>
) {
LazyVerticalGrid() { }
}
@Preview
@Composable
private fun QuizScreenPreview() {
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)
)
} }
} }

View File

@@ -3,7 +3,7 @@ package dev.adriankuta.kahootquiz.ui.quiz
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.adriankuta.kahootquiz.domain.models.Quiz 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.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -20,12 +20,12 @@ class QuizScreenViewModel @Inject constructor(
init { init {
viewModelScope.launch { viewModelScope.launch {
_uiState.value = QuizUiState(getQuizUseCase()) _uiState.value = QuizUiState(getQuizUseCase().questions.first())
} }
} }
} }
data class QuizUiState( data class QuizUiState(
val quiz: Quiz? = null val currentQuestion: Question? = null
) )

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="quiz">Quiz</string>
</resources>