mirror of
https://github.com/AdrianKuta/KahootQuiz.git
synced 2025-09-14 17:24:21 +02:00
Refactor: Extract QuizScreen composables and improve timer logic
This commit refactors `QuizScreen.kt` by extracting several composable functions for better organization and readability. It also includes improvements to the timer logic and minor UI adjustments. Key changes: - **QuizScreen.kt:** - Extracted `QuizScreenLoading` and `QuizScreenSuccess` composables to handle different UI states. - Further decomposed `QuizScreenSuccess` into `LazyListScope` extension functions for `toolbar`, `questionContent`, `choices`, `timer`, and `continueButton`. This improves the structure of the `LazyColumn` and allows for more granular recomposition. - Improved `TimerBar` logic: - Ensured `targetValue` for `animateFloatAsState` is properly calculated to avoid division by zero when `totalSeconds` is 0. - Coerced the progress `Float` value to be within `0f` and `1f` before applying it to `fillMaxWidth`. - Changed `contentDescription` of the type icon in `QuizType` to `null` as it's decorative. - Used `androidx.compose.runtime.remember` for `questionText` in `QuestionContent` to avoid unnecessary recomposition of `HtmlCompat.fromHtml`.
This commit is contained in:
@@ -22,6 +22,7 @@ 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.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
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
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@@ -92,13 +93,52 @@ private fun QuizScreen(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
when (uiState) {
|
when (uiState) {
|
||||||
ScreenUiState.Loading -> CircularProgressIndicator()
|
ScreenUiState.Loading -> QuizScreenLoading()
|
||||||
is ScreenUiState.Success -> LazyColumn(
|
is ScreenUiState.Success -> QuizScreenSuccess(
|
||||||
modifier = Modifier
|
uiState = uiState,
|
||||||
|
onSelect = onSelect,
|
||||||
|
onContinue = onContinue,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun QuizScreenLoading(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun QuizScreenSuccess(
|
||||||
|
uiState: ScreenUiState.Success,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
|
onContinue: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.animateContentSize(),
|
.animateContentSize(),
|
||||||
) {
|
) {
|
||||||
item {
|
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,
|
||||||
|
) {
|
||||||
|
item(key = "toolbar") {
|
||||||
Toolbar(
|
Toolbar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -108,37 +148,39 @@ private fun QuizScreen(
|
|||||||
totalQuestions = uiState.totalQuestions,
|
totalQuestions = uiState.totalQuestions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.questionContent(
|
||||||
|
uiState: ScreenUiState.Success,
|
||||||
|
) {
|
||||||
|
if (uiState.currentQuestion != null) {
|
||||||
|
item(key = "question_${uiState.currentQuestionIndex}") {
|
||||||
QuestionContent(
|
QuestionContent(
|
||||||
question = uiState.currentQuestion ?: return@item,
|
question = uiState.currentQuestion,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 8.dp)
|
.padding(horizontal = 8.dp)
|
||||||
.animateItem(),
|
.animateItem(),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
item {
|
|
||||||
Choices(
|
|
||||||
choices = uiState.currentQuestion?.choices
|
|
||||||
?: emptyList(), // TODO remove empty list
|
|
||||||
selectedChoiceIndex = uiState.selectedChoiceIndex,
|
|
||||||
onSelect = onSelect,
|
|
||||||
modifier = Modifier.padding(8.dp),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timer below choices
|
private fun LazyListScope.timer(uiState: ScreenUiState.Success) {
|
||||||
if (uiState.selectedChoiceIndex == null && uiState.timerState.totalTimeSeconds > 0) {
|
item(key = "timer_${uiState.currentQuestionIndex}") {
|
||||||
item {
|
|
||||||
TimerBar(
|
TimerBar(
|
||||||
totalSeconds = uiState.timerState.totalTimeSeconds,
|
totalSeconds = uiState.timerState.totalTimeSeconds,
|
||||||
remainingSeconds = uiState.timerState.remainingTimeSeconds,
|
remainingSeconds = uiState.timerState.remainingTimeSeconds,
|
||||||
modifier = Modifier.padding(8.dp),
|
modifier = Modifier.padding(8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
item {
|
|
||||||
|
private fun LazyListScope.continueButton(
|
||||||
|
uiState: ScreenUiState.Success,
|
||||||
|
onContinue: () -> Unit,
|
||||||
|
) {
|
||||||
|
item(key = "continue_${uiState.currentQuestionIndex}") {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
@@ -158,8 +200,21 @@ private fun QuizScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,7 +250,7 @@ private fun Toolbar(
|
|||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = DesignR.drawable.ic_type),
|
painter = painterResource(id = DesignR.drawable.ic_type),
|
||||||
contentDescription = "",
|
contentDescription = null,
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
@@ -224,11 +279,14 @@ private fun QuestionContent(
|
|||||||
.clip(shape = RoundedCornerShape(4.dp)),
|
.clip(shape = RoundedCornerShape(4.dp)),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Text(
|
val questionText = androidx.compose.runtime.remember(question.question) {
|
||||||
text = HtmlCompat.fromHtml(
|
HtmlCompat.fromHtml(
|
||||||
question.question ?: "",
|
question.question ?: "",
|
||||||
HtmlCompat.FROM_HTML_MODE_COMPACT,
|
HtmlCompat.FROM_HTML_MODE_COMPACT,
|
||||||
).toAnnotatedString(),
|
).toAnnotatedString()
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = questionText,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -393,15 +451,17 @@ private fun TimerBar(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val target =
|
||||||
|
if (totalSeconds <= 0) 0f else (remainingSeconds.toFloat() / totalSeconds).coerceIn(0f, 1f)
|
||||||
val progress: Float by animateFloatAsState(
|
val progress: Float by animateFloatAsState(
|
||||||
targetValue = (remainingSeconds.toFloat()) / totalSeconds,
|
targetValue = target,
|
||||||
label = "Timer",
|
label = "Timer",
|
||||||
animationSpec = tween(durationMillis = 1000, easing = LinearEasing),
|
animationSpec = tween(durationMillis = 1000, easing = LinearEasing),
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth(progress)
|
.fillMaxWidth(progress.coerceIn(0f, 1f))
|
||||||
.background(
|
.background(
|
||||||
color = Purple,
|
color = Purple,
|
||||||
shape = RoundedCornerShape(percent = 50),
|
shape = RoundedCornerShape(percent = 50),
|
||||||
|
Reference in New Issue
Block a user