mirror of
https://github.com/AdrianKuta/KahootQuiz.git
synced 2025-09-14 17:24:21 +02:00
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:
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -26,6 +26,7 @@
|
||||
<option value="$PROJECT_DIR$/build-logic" />
|
||||
<option value="$PROJECT_DIR$/build-logic/convention" />
|
||||
<option value="$PROJECT_DIR$/core" />
|
||||
<option value="$PROJECT_DIR$/core/designsystem" />
|
||||
<option value="$PROJECT_DIR$/core/network" />
|
||||
<option value="$PROJECT_DIR$/domain" />
|
||||
<option value="$PROJECT_DIR$/model" />
|
||||
|
@@ -27,10 +27,10 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.ui.quiz)
|
||||
implementation(projects.core.designsystem)
|
||||
implementation(projects.domain)
|
||||
|
||||
implementation(projects.model.data)
|
||||
implementation(projects.ui.quiz)
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
|
@@ -5,7 +5,7 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.adriankuta.kahootquiz.ui.theme.KahootQuizTheme
|
||||
import dev.adriankuta.kahootquiz.core.designsystem.KahootQuizTheme
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
7
core/designsystem/build.gradle.kts
Normal file
7
core/designsystem/build.gradle.kts
Normal file
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kahootquiz.android.library.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.adriankuta.kahootquiz.core.designsystem"
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package dev.adriankuta.kahootquiz.ui.theme
|
||||
package dev.adriankuta.kahootquiz.core.designsystem
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@@ -8,4 +8,6 @@ val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
|
||||
val Grey = Color(0xFFFAFAFA)
|
@@ -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 androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
@@ -1,4 +1,4 @@
|
||||
package dev.adriankuta.kahootquiz.ui.theme
|
||||
package dev.adriankuta.kahootquiz.core.designsystem
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
BIN
core/designsystem/src/main/res/drawable/bg_image.webp
Normal file
BIN
core/designsystem/src/main/res/drawable/bg_image.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
50
core/designsystem/src/main/res/drawable/ic_type.xml
Normal file
50
core/designsystem/src/main/res/drawable/ic_type.xml
Normal 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>
|
@@ -3,5 +3,5 @@ package dev.adriankuta.kahootquiz.domain.models
|
||||
data class Choice(
|
||||
val answer: String?,
|
||||
val correct: Boolean?,
|
||||
val languageInfo: LanguageInfo?
|
||||
val languageInfo: LanguageInfo? = null
|
||||
)
|
@@ -8,16 +8,16 @@ data class Question(
|
||||
val type: String?,
|
||||
val question: String?,
|
||||
val time: Duration?,
|
||||
val points: Boolean?,
|
||||
val points: Boolean? = null,
|
||||
val pointsMultiplier: Int?,
|
||||
val choices: List<Choice>?,
|
||||
val layout: String?,
|
||||
val image: String?,
|
||||
val layout: String? = null,
|
||||
val image: String,
|
||||
val imageMetadata: ImageMetadata?,
|
||||
val resources: String?,
|
||||
val video: Video?,
|
||||
val questionFormat: Int?,
|
||||
val languageInfo: LanguageInfo?,
|
||||
val media: List<MediaItem>?,
|
||||
val choiceRange: ChoiceRange?
|
||||
val resources: String? = null,
|
||||
val video: Video? = null,
|
||||
val questionFormat: Int? = null,
|
||||
val languageInfo: LanguageInfo? = null,
|
||||
val media: List<MediaItem>? = null,
|
||||
val choiceRange: ChoiceRange? = null
|
||||
)
|
@@ -1,4 +1,6 @@
|
||||
[versions]
|
||||
coilCompose = "3.3.0"
|
||||
coilNetworkOkhttp = "3.3.0"
|
||||
retrofit = "3.0.0"
|
||||
targetSdk = "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" }
|
||||
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
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-ktlint = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
|
||||
dotlottie-android = { module = "com.github.LottieFiles:dotlottie-android", version.ref = "dotlottieAndroid" }
|
||||
|
@@ -25,6 +25,7 @@ rootProject.name = "KahootQuiz"
|
||||
|
||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||
include(":app")
|
||||
include(":core:designsystem")
|
||||
include(":core:network")
|
||||
include(":domain")
|
||||
include(":model:data")
|
||||
|
@@ -9,7 +9,11 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.designsystem)
|
||||
implementation(projects.domain)
|
||||
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.network.okhttp)
|
||||
}
|
||||
|
@@ -1,12 +1,40 @@
|
||||
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.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.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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.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
|
||||
fun QuizScreen(
|
||||
@@ -18,7 +46,7 @@ fun QuizScreen(
|
||||
|
||||
QuizScreen(
|
||||
uiState = uiState,
|
||||
modifier = modifier
|
||||
modifier = modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,7 +55,132 @@ private fun QuizScreen(
|
||||
uiState: QuizUiState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier) {
|
||||
Text(uiState.quiz?.id?.value ?: "")
|
||||
Box(modifier.fillMaxSize()) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ package dev.adriankuta.kahootquiz.ui.quiz
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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 kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -20,12 +20,12 @@ class QuizScreenViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = QuizUiState(getQuizUseCase())
|
||||
_uiState.value = QuizUiState(getQuizUseCase().questions.first())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class QuizUiState(
|
||||
val quiz: Quiz? = null
|
||||
val currentQuestion: Question? = null
|
||||
)
|
4
ui/quiz/src/main/res/values/strings.xml
Normal file
4
ui/quiz/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="quiz">Quiz</string>
|
||||
</resources>
|
Reference in New Issue
Block a user