mirror of
https://github.com/AdrianKuta/KahootQuiz.git
synced 2025-09-14 17:24:21 +02:00
feat: Refactor ViewModel state production and add lint baselines
This commit refactors how `ScreenUiState` is produced in `QuizScreenViewModel` by extracting the logic into a private `screenUiState` function that combines various flows. It also introduces `Result` sealed interface and `asResult()` Flow extension for handling asynchronous operations. Additionally, lint baseline files have been added to all modules and a Detekt configuration file for the `ui:quiz` module. Key changes: - **ViewModel Refactoring (`ui:quiz` module):** - Introduced `Result.kt` with a sealed interface for `Success`, `Error`, and `Loading` states, along with an `asResult()` extension function for `Flow` to wrap emissions in `Result`. - In `QuizScreenViewModel.kt`: - The `quiz` state flow now uses `asResult()` and maps the `Result` to `QuizUiState`, handling potential error states by defaulting to `QuizUiState.Loading` (actual error UI handling is noted as a TODO). - The logic for combining flows to produce `ScreenUiState` has been extracted into a new private function `screenUiState()`. This function takes the necessary flows as parameters and returns a `Flow<ScreenUiState>`. - The `uiState` flow now uses this new `screenUiState()` function for its production. - **Lint and Detekt Configuration:** - Added `lint-baseline.xml` files to the following modules to establish a baseline for lint warnings: - `app` - `core/designsystem` - `core/network` - `domain` - `model/data` - `ui/quiz` - Added `config/detekt/detekt.yml` to the `ui:quiz` module to configure Detekt rules, including exceptions for Jetpack Compose naming conventions, complexity rules for Composable functions, and style guidelines for magic numbers and unused private members. It also enables trailing commas for call sites and declaration sites. - **Binary File:** - Added `App.apk`.
This commit is contained in:
33
ui/quiz/config/detekt/detekt.yml
Normal file
33
ui/quiz/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
|
4
ui/quiz/lint-baseline.xml
Normal file
4
ui/quiz/lint-baseline.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.13.0-rc02" type="baseline" client="gradle" dependencies="false" name="AGP (8.13.0-rc02)" variant="all" version="8.13.0-rc02">
|
||||
|
||||
</issues>
|
@@ -6,13 +6,17 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.adriankuta.kahootquiz.domain.models.Question
|
||||
import dev.adriankuta.kahootquiz.domain.models.Quiz
|
||||
import dev.adriankuta.kahootquiz.domain.usecases.GetQuizUseCase
|
||||
import dev.adriankuta.kahootquiz.ui.quiz.utils.Result
|
||||
import dev.adriankuta.kahootquiz.ui.quiz.utils.asResult
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@@ -26,6 +30,14 @@ class QuizScreenViewModel @Inject constructor(
|
||||
private val quiz: StateFlow<QuizUiState> = flow {
|
||||
emit(QuizUiState.Success(getQuizUseCase()))
|
||||
}
|
||||
.asResult()
|
||||
.map { quizResult ->
|
||||
when (quizResult) {
|
||||
is Result.Error -> QuizUiState.Loading // Todo error handling not implemented on UI
|
||||
Result.Loading -> QuizUiState.Loading
|
||||
is Result.Success -> quizResult.data
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
@@ -52,34 +64,12 @@ class QuizScreenViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val uiState: StateFlow<ScreenUiState> = combine(
|
||||
quiz,
|
||||
_selectedChoiceIndex,
|
||||
_remainingTimeSeconds,
|
||||
_currentQuestionIndex,
|
||||
) { quizState, selectedChoiceIndex, remainingTimeSeconds, currentQuestionIndex ->
|
||||
when (quizState) {
|
||||
QuizUiState.Loading -> ScreenUiState.Loading
|
||||
is QuizUiState.Success -> {
|
||||
val currentQuestion = quizState.quiz.questions.getOrNull(currentQuestionIndex)
|
||||
val isAnswerCorrect = selectedChoiceIndex?.let { idx ->
|
||||
currentQuestion?.choices?.getOrNull(idx)?.correct == true
|
||||
}
|
||||
|
||||
ScreenUiState.Success(
|
||||
currentQuestion = currentQuestion,
|
||||
selectedChoiceIndex = selectedChoiceIndex,
|
||||
currentQuestionIndex = currentQuestionIndex,
|
||||
totalQuestions = quizState.quiz.questions.size,
|
||||
timerState = TimerState(
|
||||
remainingTimeSeconds = remainingTimeSeconds,
|
||||
totalTimeSeconds = currentQuestion?.time?.inWholeSeconds?.toInt() ?: 0,
|
||||
),
|
||||
isAnswerCorrect = isAnswerCorrect,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val uiState: StateFlow<ScreenUiState> = screenUiState(
|
||||
quizFlow = quiz,
|
||||
selectedChoiceIndexFlow = _selectedChoiceIndex,
|
||||
remainingTimeSecondsFlow = _remainingTimeSeconds,
|
||||
currentQuestionIndexFlow = _currentQuestionIndex,
|
||||
)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
@@ -137,6 +127,40 @@ class QuizScreenViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun screenUiState(
|
||||
quizFlow: StateFlow<QuizUiState>,
|
||||
selectedChoiceIndexFlow: Flow<Int?>,
|
||||
remainingTimeSecondsFlow: Flow<Int>,
|
||||
currentQuestionIndexFlow: Flow<Int>,
|
||||
): Flow<ScreenUiState> = combine(
|
||||
quizFlow,
|
||||
selectedChoiceIndexFlow,
|
||||
remainingTimeSecondsFlow,
|
||||
currentQuestionIndexFlow,
|
||||
) { quizState, selectedChoiceIndex, remainingTimeSeconds, currentQuestionIndex ->
|
||||
when (quizState) {
|
||||
QuizUiState.Loading -> ScreenUiState.Loading
|
||||
is QuizUiState.Success -> {
|
||||
val currentQuestion = quizState.quiz.questions.getOrNull(currentQuestionIndex)
|
||||
val isAnswerCorrect = selectedChoiceIndex?.let { idx ->
|
||||
currentQuestion?.choices?.getOrNull(idx)?.correct == true
|
||||
}
|
||||
|
||||
ScreenUiState.Success(
|
||||
currentQuestion = currentQuestion,
|
||||
selectedChoiceIndex = selectedChoiceIndex,
|
||||
currentQuestionIndex = currentQuestionIndex,
|
||||
totalQuestions = quizState.quiz.questions.size,
|
||||
timerState = TimerState(
|
||||
remainingTimeSeconds = remainingTimeSeconds,
|
||||
totalTimeSeconds = currentQuestion?.time?.inWholeSeconds?.toInt() ?: 0,
|
||||
),
|
||||
isAnswerCorrect = isAnswerCorrect,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface QuizUiState {
|
||||
data object Loading : QuizUiState
|
||||
data class Success(
|
||||
|
@@ -0,0 +1,16 @@
|
||||
package dev.adriankuta.kahootquiz.ui.quiz.utils
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
||||
sealed interface Result<out T> {
|
||||
data class Success<T>(val data: T) : Result<T>
|
||||
data class Error(val exception: Throwable) : Result<Nothing>
|
||||
data object Loading : Result<Nothing>
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.asResult(): Flow<Result<T>> = map<T, Result<T>> { Result.Success(it) }
|
||||
.onStart { emit(Result.Loading) }
|
||||
.catch { emit(Result.Error(it)) }
|
Reference in New Issue
Block a user