diff --git a/App.apk b/App.apk new file mode 100644 index 0000000..5618693 Binary files /dev/null and b/App.apk differ diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml new file mode 100644 index 0000000..2e47dc4 --- /dev/null +++ b/app/lint-baseline.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/designsystem/lint-baseline.xml b/core/designsystem/lint-baseline.xml new file mode 100644 index 0000000..3a4b296 --- /dev/null +++ b/core/designsystem/lint-baseline.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/network/lint-baseline.xml b/core/network/lint-baseline.xml new file mode 100644 index 0000000..ab0424c --- /dev/null +++ b/core/network/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/domain/lint-baseline.xml b/domain/lint-baseline.xml new file mode 100644 index 0000000..ab0424c --- /dev/null +++ b/domain/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/model/data/lint-baseline.xml b/model/data/lint-baseline.xml new file mode 100644 index 0000000..ab0424c --- /dev/null +++ b/model/data/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/ui/quiz/config/detekt/detekt.yml b/ui/quiz/config/detekt/detekt.yml new file mode 100644 index 0000000..aba4110 --- /dev/null +++ b/ui/quiz/config/detekt/detekt.yml @@ -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 \ No newline at end of file diff --git a/ui/quiz/lint-baseline.xml b/ui/quiz/lint-baseline.xml new file mode 100644 index 0000000..ab0424c --- /dev/null +++ b/ui/quiz/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/QuizScreenViewModel.kt b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/QuizScreenViewModel.kt index 0860cb5..5bd0c39 100644 --- a/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/QuizScreenViewModel.kt +++ b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/QuizScreenViewModel.kt @@ -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 = 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 = 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( + 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, + selectedChoiceIndexFlow: Flow, + remainingTimeSecondsFlow: Flow, + currentQuestionIndexFlow: Flow, +): Flow = 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( diff --git a/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/utils/Result.kt b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/utils/Result.kt new file mode 100644 index 0000000..a5f34b6 --- /dev/null +++ b/ui/quiz/src/main/kotlin/dev/adriankuta/kahootquiz/ui/quiz/utils/Result.kt @@ -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 { + data class Success(val data: T) : Result + data class Error(val exception: Throwable) : Result + data object Loading : Result +} + +fun Flow.asResult(): Flow> = map> { Result.Success(it) } + .onStart { emit(Result.Loading) } + .catch { emit(Result.Error(it)) }