mirror of
https://github.com/AdrianKuta/KahootQuiz.git
synced 2025-09-15 01:24:23 +02:00
refactor: Improve QuizScreen layout and choice presentation
This commit refactors the `QuizScreen` to use a `Column` instead of a `LazyColumn` for its main layout, improving the positioning and sizing of elements. It also introduces an `EvenGrid` composable for displaying choices, ensuring they are evenly distributed. Key changes: - **QuizScreen.kt:** - Changed the main layout from `LazyColumn` to `Column`. - Toolbar, QuestionContent, Choices, Timer, and ContinueButton are now direct children of the `Column`. - `QuestionContent` now uses `fillMaxHeight(0.5f)` to take up half the available height. - `Choices` composable now uses `weight(1f)` to fill remaining space. - Removed `animateContentSize` and `animateItem` modifiers. - Refactored how individual UI sections (toolbar, question, choices, timer, continue button) are structured within the main `Column`. - **components/EvenGrid.kt:** - Added a new reusable `EvenGrid` composable that arranges items into a grid with a specified number of columns, ensuring even distribution. - **components/Choices.kt:** - Replaced `FlowRow` with the new `EvenGrid` composable to display choices. - `ChoiceItem` now uses `fillMaxHeight()` within the `EvenGrid` cell. - Removed explicit height setting for `ChoiceItem`'s `Box`. - **components/QuestionContent.kt:** - `AsyncImage` within `QuestionContent` now uses `weight(1f)` instead of `heightIn(min = 200.dp)`. - **components/TimerBar.kt:** - Removed `coerceIn(0f, 1f)` for `progress` in `fillMaxWidth` as progress should already be within this range. - **QuizScreenViewModel.kt & domain/models/Question.kt:** - Made `Question.choices` non-nullable (`List<Choice>`) and updated the mapper and ViewModel to reflect this. This simplifies null checks for `currentQuestion.choices`. - Accessing `quizState.quiz.questions[currentQuestionIndex]` directly instead of `getOrNull` as the index should always be valid. - **data/mappers/QuizMapper.kt:** - Updated `QuestionDto.toDomain()` to return `orEmpty()` for choices, ensuring a non-null list.
This commit is contained in:
@@ -153,7 +153,7 @@ private fun QuestionDto.toDomain(): Question = Question(
|
|||||||
time = time?.milliseconds,
|
time = time?.milliseconds,
|
||||||
points = points,
|
points = points,
|
||||||
pointsMultiplier = pointsMultiplier,
|
pointsMultiplier = pointsMultiplier,
|
||||||
choices = choices?.map { it.toDomain() },
|
choices = choices?.map { it.toDomain() }.orEmpty(),
|
||||||
layout = layout,
|
layout = layout,
|
||||||
image = image,
|
image = image,
|
||||||
imageMetadata = imageMetadata?.toDomain(),
|
imageMetadata = imageMetadata?.toDomain(),
|
||||||
|
@@ -10,7 +10,7 @@ data class Question(
|
|||||||
val time: Duration?,
|
val time: Duration?,
|
||||||
val points: Boolean? = null,
|
val points: Boolean? = null,
|
||||||
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? = null,
|
val image: String? = null,
|
||||||
val imageMetadata: ImageMetadata?,
|
val imageMetadata: ImageMetadata?,
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
package dev.adriankuta.kahootquiz.ui.quiz
|
package dev.adriankuta.kahootquiz.ui.quiz
|
||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
@@ -85,28 +85,10 @@ private fun QuizScreenSuccess(
|
|||||||
onContinue: () -> Unit,
|
onContinue: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(),
|
||||||
.animateContentSize(),
|
|
||||||
) {
|
) {
|
||||||
toolbar(uiState)
|
|
||||||
questionContent(uiState)
|
|
||||||
choices(uiState, onSelect)
|
|
||||||
// Timer below choices
|
|
||||||
if (uiState.selectedChoiceIndex == null && uiState.timerState.totalTimeSeconds > 0) {
|
|
||||||
timer(uiState)
|
|
||||||
} else {
|
|
||||||
continueButton(uiState, onContinue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun LazyListScope.toolbar(
|
|
||||||
uiState: ScreenUiState.Success,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
item(key = "toolbar") {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.height(72.dp),
|
.height(72.dp),
|
||||||
@@ -124,25 +106,56 @@ private fun LazyListScope.toolbar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun LazyListScope.questionContent(
|
QuestionContent(
|
||||||
uiState: ScreenUiState.Success,
|
question = uiState.currentQuestion,
|
||||||
) {
|
modifier = Modifier
|
||||||
if (uiState.currentQuestion != null) {
|
.padding(horizontal = 8.dp)
|
||||||
item(key = "question_${uiState.currentQuestionIndex}") {
|
.fillMaxHeight(0.5f),
|
||||||
QuestionContent(
|
)
|
||||||
question = uiState.currentQuestion,
|
Spacer(Modifier.height(8.dp))
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.animateItem(),
|
Choices(
|
||||||
|
choices = uiState.currentQuestion.choices,
|
||||||
|
selectedChoiceIndex = uiState.selectedChoiceIndex,
|
||||||
|
onSelect = onSelect,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.weight(1f),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Timer below choices
|
||||||
|
if (uiState.selectedChoiceIndex == null && uiState.timerState.totalTimeSeconds > 0) {
|
||||||
|
TimerBar(
|
||||||
|
totalSeconds = uiState.timerState.totalTimeSeconds,
|
||||||
|
remainingSeconds = uiState.timerState.remainingTimeSeconds,
|
||||||
|
modifier = Modifier.padding(8.dp),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = onContinue,
|
||||||
|
colors = ButtonDefaults.filledTonalButtonColors().copy(
|
||||||
|
containerColor = Grey,
|
||||||
|
contentColor = Color.Black,
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.continue_text),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun LazyListScope.timer(uiState: ScreenUiState.Success) {
|
private fun LazyListScope.timer(uiState: ScreenUiState.Success) {
|
||||||
item(key = "timer_${uiState.currentQuestionIndex}") {
|
item(key = "timer_${uiState.currentQuestionIndex}") {
|
||||||
TimerBar(
|
TimerBar(
|
||||||
@@ -153,49 +166,6 @@ private fun LazyListScope.timer(uiState: ScreenUiState.Success) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun LazyListScope.continueButton(
|
|
||||||
uiState: ScreenUiState.Success,
|
|
||||||
onContinue: () -> Unit,
|
|
||||||
) {
|
|
||||||
item(key = "continue_${uiState.currentQuestionIndex}") {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
FilledTonalButton(
|
|
||||||
onClick = onContinue,
|
|
||||||
colors = ButtonDefaults.filledTonalButtonColors().copy(
|
|
||||||
containerColor = Grey,
|
|
||||||
contentColor = Color.Black,
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(4.dp),
|
|
||||||
modifier = Modifier.align(Alignment.Center),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.continue_text),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun LazyListScope.choices(
|
|
||||||
uiState: ScreenUiState.Success,
|
|
||||||
onSelect: (Int) -> Unit,
|
|
||||||
) {
|
|
||||||
uiState.currentQuestion?.choices?.let { choicesList ->
|
|
||||||
if (choicesList.isNotEmpty()) {
|
|
||||||
item(key = "choices_${uiState.currentQuestionIndex}") {
|
|
||||||
Choices(
|
|
||||||
choices = choicesList,
|
|
||||||
selectedChoiceIndex = uiState.selectedChoiceIndex,
|
|
||||||
onSelect = onSelect,
|
|
||||||
modifier = Modifier.padding(8.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun QuizScreenPreview() {
|
private fun QuizScreenPreview() {
|
||||||
|
@@ -141,9 +141,9 @@ private fun screenUiState(
|
|||||||
when (quizState) {
|
when (quizState) {
|
||||||
QuizUiState.Loading -> ScreenUiState.Loading
|
QuizUiState.Loading -> ScreenUiState.Loading
|
||||||
is QuizUiState.Success -> {
|
is QuizUiState.Success -> {
|
||||||
val currentQuestion = quizState.quiz.questions.getOrNull(currentQuestionIndex)
|
val currentQuestion = quizState.quiz.questions[currentQuestionIndex]
|
||||||
val isAnswerCorrect = selectedChoiceIndex?.let { idx ->
|
val isAnswerCorrect = selectedChoiceIndex?.let { idx ->
|
||||||
currentQuestion?.choices?.getOrNull(idx)?.correct == true
|
currentQuestion.choices?.getOrNull(idx)?.correct == true
|
||||||
}
|
}
|
||||||
|
|
||||||
ScreenUiState.Success(
|
ScreenUiState.Success(
|
||||||
@@ -153,7 +153,7 @@ private fun screenUiState(
|
|||||||
totalQuestions = quizState.quiz.questions.size,
|
totalQuestions = quizState.quiz.questions.size,
|
||||||
timerState = TimerState(
|
timerState = TimerState(
|
||||||
remainingTimeSeconds = remainingTimeSeconds,
|
remainingTimeSeconds = remainingTimeSeconds,
|
||||||
totalTimeSeconds = currentQuestion?.time?.inWholeSeconds?.toInt() ?: 0,
|
totalTimeSeconds = currentQuestion.time?.inWholeSeconds?.toInt() ?: 0,
|
||||||
),
|
),
|
||||||
isAnswerCorrect = isAnswerCorrect,
|
isAnswerCorrect = isAnswerCorrect,
|
||||||
)
|
)
|
||||||
@@ -171,7 +171,7 @@ sealed interface QuizUiState {
|
|||||||
sealed interface ScreenUiState {
|
sealed interface ScreenUiState {
|
||||||
data object Loading : ScreenUiState
|
data object Loading : ScreenUiState
|
||||||
data class Success(
|
data class Success(
|
||||||
val currentQuestion: Question? = null,
|
val currentQuestion: Question,
|
||||||
val selectedChoiceIndex: Int? = null,
|
val selectedChoiceIndex: Int? = null,
|
||||||
val currentQuestionIndex: Int = 0,
|
val currentQuestionIndex: Int = 0,
|
||||||
val totalQuestions: Int = 0,
|
val totalQuestions: Int = 0,
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
@file:OptIn(ExperimentalLayoutApi::class)
|
||||||
|
|
||||||
package dev.adriankuta.kahootquiz.ui.quiz.components
|
package dev.adriankuta.kahootquiz.ui.quiz.components
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@@ -5,8 +7,8 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.offset
|
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
|
||||||
@@ -37,21 +39,22 @@ fun Choices(
|
|||||||
selectedChoiceIndex: Int?,
|
selectedChoiceIndex: Int?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
FlowRow(
|
EvenGrid(
|
||||||
maxItemsInEachRow = 2,
|
items = choices,
|
||||||
|
columns = 2,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
choices.forEachIndexed { index, choice ->
|
) { choice, index ->
|
||||||
ChoiceItem(
|
ChoiceItem(
|
||||||
choice = choice,
|
choice = choice,
|
||||||
index = index,
|
index = index,
|
||||||
selectedChoiceIndex = selectedChoiceIndex,
|
selectedChoiceIndex = selectedChoiceIndex,
|
||||||
onClick = { onSelect(index) },
|
onClick = { onSelect(index) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
)
|
.weight(1f)
|
||||||
}
|
.fillMaxHeight(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +107,6 @@ private fun ChoiceItemDefault(
|
|||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(backgroundColor, shape = RoundedCornerShape(4.dp))
|
.background(backgroundColor, shape = RoundedCornerShape(4.dp))
|
||||||
.height(100.dp)
|
|
||||||
.clickable(
|
.clickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
),
|
),
|
||||||
@@ -152,8 +154,7 @@ private fun ChoiceItemRevealed(
|
|||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(backgroundColor, shape = RoundedCornerShape(4.dp))
|
.background(backgroundColor, shape = RoundedCornerShape(4.dp)),
|
||||||
.height(100.dp),
|
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(icon),
|
painter = painterResource(icon),
|
||||||
|
@@ -0,0 +1,39 @@
|
|||||||
|
package dev.adriankuta.kahootquiz.ui.quiz.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> EvenGrid(
|
||||||
|
items: List<T>,
|
||||||
|
columns: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||||
|
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
|
||||||
|
content: @Composable RowScope.(item: T, index: Int) -> Unit,
|
||||||
|
) {
|
||||||
|
val rows = (items.size + columns - 1) / columns // total rows needed
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalArrangement = verticalArrangement,
|
||||||
|
) {
|
||||||
|
repeat(rows) { rowIndex ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
horizontalArrangement = horizontalArrangement,
|
||||||
|
) {
|
||||||
|
repeat(columns) { columnIndex ->
|
||||||
|
val itemIndex = rowIndex * columns + columnIndex
|
||||||
|
if (itemIndex < items.size) {
|
||||||
|
content(items[itemIndex], itemIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -34,8 +33,7 @@ fun QuestionContent(
|
|||||||
contentDescription = question.imageMetadata?.altText,
|
contentDescription = question.imageMetadata?.altText,
|
||||||
contentScale = ContentScale.FillWidth,
|
contentScale = ContentScale.FillWidth,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.weight(1f)
|
||||||
.heightIn(min = 200.dp)
|
|
||||||
.clip(shape = RoundedCornerShape(4.dp)),
|
.clip(shape = RoundedCornerShape(4.dp)),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
@@ -33,7 +33,7 @@ fun TimerBar(
|
|||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth(progress.coerceIn(0f, 1f))
|
.fillMaxWidth(progress)
|
||||||
.background(
|
.background(
|
||||||
color = Purple,
|
color = Purple,
|
||||||
shape = RoundedCornerShape(percent = 50),
|
shape = RoundedCornerShape(percent = 50),
|
||||||
|
Reference in New Issue
Block a user