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:
2025-09-04 22:41:06 +02:00
parent 77a3dd9eeb
commit 59218cc2e1
10 changed files with 451 additions and 28 deletions

BIN
App.apk Normal file

Binary file not shown.

158
app/lint-baseline.xml Normal file
View File

@@ -0,0 +1,158 @@
<?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">
<issue
id="UnusedAttribute"
message="Attribute `endX` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:endX=&quot;85.84757&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_launcher_foreground.xml"
line="10"
column="17"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `endY` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:endY=&quot;92.4963&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_launcher_foreground.xml"
line="11"
column="17"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `startX` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:startX=&quot;42.9492&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_launcher_foreground.xml"
line="12"
column="17"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `startY` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:startY=&quot;49.59793&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_launcher_foreground.xml"
line="13"
column="17"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `offset` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:offset=&quot;0.0&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_launcher_foreground.xml"
line="17"
column="21"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `offset` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:offset=&quot;1.0&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_launcher_foreground.xml"
line="20"
column="21"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;nonZero&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_launcher_foreground.xml"
line="26"
column="9"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.color.purple_200` appears to be unused"
errorLine1=" &lt;color name=&quot;purple_200&quot;>#FFBB86FC&lt;/color>"
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/colors.xml"
line="3"
column="12"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.color.purple_500` appears to be unused"
errorLine1=" &lt;color name=&quot;purple_500&quot;>#FF6200EE&lt;/color>"
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/colors.xml"
line="4"
column="12"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.color.purple_700` appears to be unused"
errorLine1=" &lt;color name=&quot;purple_700&quot;>#FF3700B3&lt;/color>"
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/colors.xml"
line="5"
column="12"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.color.teal_200` appears to be unused"
errorLine1=" &lt;color name=&quot;teal_200&quot;>#FF03DAC5&lt;/color>"
errorLine2=" ~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/colors.xml"
line="6"
column="12"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.color.teal_700` appears to be unused"
errorLine1=" &lt;color name=&quot;teal_700&quot;>#FF018786&lt;/color>"
errorLine2=" ~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/colors.xml"
line="7"
column="12"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.color.black` appears to be unused"
errorLine1=" &lt;color name=&quot;black&quot;>#FF000000&lt;/color>"
errorLine2=" ~~~~~~~~~~~~">
<location
file="src/main/res/values/colors.xml"
line="8"
column="12"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.color.white` appears to be unused"
errorLine1=" &lt;color name=&quot;white&quot;>#FFFFFFFF&lt;/color>"
errorLine2=" ~~~~~~~~~~~~">
<location
file="src/main/res/values/colors.xml"
line="9"
column="12"/>
</issue>
</issues>

View File

@@ -0,0 +1,176 @@
<?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">
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;evenOdd&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_type.xml"
line="8"
column="9"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;evenOdd&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_type.xml"
line="12"
column="9"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;evenOdd&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_type.xml"
line="16"
column="9"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;evenOdd&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_type.xml"
line="20"
column="9"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;evenOdd&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_type.xml"
line="24"
column="9"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;evenOdd&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_type.xml"
line="28"
column="9"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;evenOdd&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_type.xml"
line="32"
column="9"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;evenOdd&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_type.xml"
line="36"
column="9"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;evenOdd&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_type.xml"
line="40"
column="9"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;evenOdd&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_type.xml"
line="44"
column="9"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;evenOdd&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_type.xml"
line="48"
column="9"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;evenOdd&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_wrong.xml"
line="22"
column="9"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `fillType` is only used in API level 24 and higher (current min is 23)"
errorLine1=" android:fillType=&quot;evenOdd&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_wrong.xml"
line="27"
column="13"/>
</issue>
<issue
id="VectorRaster"
message="This tag is not supported in images generated from this vector icon for API &lt; 24; check generated icon to make sure it looks acceptable"
errorLine1=" &lt;clip-path android:pathData=&quot;M5.853,5.721h28.284v28.284h-28.284z&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_wrong.xml"
line="25"
column="9"/>
</issue>
<issue
id="VectorRaster"
message="This tag is not supported in images generated from this vector icon for API &lt; 24; check generated icon to make sure it looks acceptable"
errorLine1=" &lt;clip-path"
errorLine2=" ^">
<location
file="src/main/res/drawable/ic_wrong.xml"
line="26"
column="9"/>
</issue>
<issue
id="IconLocation"
message="Found bitmap drawable `res/drawable/bg_image.webp` in densityless folder">
<location
file="src/main/res/drawable/bg_image.webp"/>
</issue>
</issues>

View 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>

4
domain/lint-baseline.xml Normal file
View 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>

View 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>

View 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

View 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>

View File

@@ -6,13 +6,17 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dev.adriankuta.kahootquiz.domain.models.Question import dev.adriankuta.kahootquiz.domain.models.Question
import dev.adriankuta.kahootquiz.domain.models.Quiz import dev.adriankuta.kahootquiz.domain.models.Quiz
import dev.adriankuta.kahootquiz.domain.usecases.GetQuizUseCase 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.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -26,6 +30,14 @@ class QuizScreenViewModel @Inject constructor(
private val quiz: StateFlow<QuizUiState> = flow { private val quiz: StateFlow<QuizUiState> = flow {
emit(QuizUiState.Success(getQuizUseCase())) 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( .stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000), started = SharingStarted.WhileSubscribed(5_000),
@@ -52,34 +64,12 @@ class QuizScreenViewModel @Inject constructor(
} }
} }
val uiState: StateFlow<ScreenUiState> = combine( val uiState: StateFlow<ScreenUiState> = screenUiState(
quiz, quizFlow = quiz,
_selectedChoiceIndex, selectedChoiceIndexFlow = _selectedChoiceIndex,
_remainingTimeSeconds, remainingTimeSecondsFlow = _remainingTimeSeconds,
_currentQuestionIndex, currentQuestionIndexFlow = _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,
)
}
}
}
.stateIn( .stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000), 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 { sealed interface QuizUiState {
data object Loading : QuizUiState data object Loading : QuizUiState
data class Success( data class Success(

View File

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