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)) }