Skip to content

Commit d8fe2a3

Browse files
committed
refactor(quiz): extract QuizTimerController from QuizViewModel
1 parent f0cb9b9 commit d8fe2a3

4 files changed

Lines changed: 76 additions & 28 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.leanite.dynaquiz.feature.quiz
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.Job
5+
import kotlinx.coroutines.delay
6+
import kotlinx.coroutines.flow.MutableStateFlow
7+
import kotlinx.coroutines.flow.StateFlow
8+
import kotlinx.coroutines.flow.asStateFlow
9+
import kotlinx.coroutines.launch
10+
import kotlin.time.Duration.Companion.seconds
11+
12+
class QuizTimerController {
13+
private val _timeRemaining = MutableStateFlow<Int?>(null)
14+
val timeRemaining: StateFlow<Int?> = _timeRemaining.asStateFlow()
15+
16+
private var job: Job? = null
17+
18+
fun start(
19+
scope: CoroutineScope,
20+
durationSec: Int,
21+
onTimeout: () -> Unit,
22+
) {
23+
job?.cancel()
24+
job =
25+
scope.launch {
26+
for (sec in durationSec downTo 1) {
27+
_timeRemaining.value = sec
28+
delay(1.seconds)
29+
}
30+
_timeRemaining.value = 0
31+
onTimeout()
32+
}
33+
}
34+
35+
fun stop() {
36+
cancel()
37+
_timeRemaining.value = null
38+
}
39+
40+
fun cancel() {
41+
job?.cancel()
42+
}
43+
44+
suspend fun runCountdown(
45+
secondsFrom: Int,
46+
onTick: (Int) -> Unit,
47+
) {
48+
for (sec in secondsFrom downTo 1) {
49+
onTick(sec)
50+
delay(1.seconds)
51+
}
52+
}
53+
}

dynaquiz/composeApp/src/commonMain/kotlin/com/leanite/dynaquiz/feature/quiz/QuizViewModel.kt

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,39 @@ import com.leanite.dynaquiz.core.domain.usecase.SaveQuizSessionUseCase
1717
import com.leanite.dynaquiz.core.domain.usecase.SubmitAnswerUseCase
1818
import kotlinx.collections.immutable.toImmutableList
1919
import kotlinx.coroutines.Deferred
20-
import kotlinx.coroutines.Job
2120
import kotlinx.coroutines.async
2221
import kotlinx.coroutines.channels.Channel
23-
import kotlinx.coroutines.delay
2422
import kotlinx.coroutines.flow.MutableStateFlow
2523
import kotlinx.coroutines.flow.StateFlow
2624
import kotlinx.coroutines.flow.asStateFlow
2725
import kotlinx.coroutines.flow.receiveAsFlow
2826
import kotlinx.coroutines.flow.update
2927
import kotlinx.coroutines.launch
30-
import kotlin.time.Duration.Companion.seconds
3128

3229
class QuizViewModel(
3330
private val setup: QuizSetup,
3431
private val getRandomQuestionUseCase: GetRandomQuestionUseCase,
3532
private val submitAnswerUseCase: SubmitAnswerUseCase,
3633
private val saveQuizSessionUseCase: SaveQuizSessionUseCase,
34+
private val timer: QuizTimerController,
3735
) : ViewModel() {
3836
private val _uiState = MutableStateFlow(QuizUiState(challengeMode = setup.challengeMode))
3937
val uiState: StateFlow<QuizUiState> = _uiState.asStateFlow()
4038

4139
private val _events = Channel<QuizEvent>(Channel.BUFFERED)
4240
val events = _events.receiveAsFlow()
4341

44-
private var timerJob: Job? = null
4542
private var prefetchedNextQuestion: Deferred<Question?>? = null
4643

44+
init {
45+
// Reflete o timer no UiState sem o VM gerenciar a coroutine
46+
viewModelScope.launch {
47+
timer.timeRemaining.collect { sec ->
48+
_uiState.update { it.copy(timeRemainingSec = sec) }
49+
}
50+
}
51+
}
52+
4753
fun onIntent(intent: QuizIntent) {
4854
when (intent) {
4955
QuizIntent.Started -> start()
@@ -73,38 +79,23 @@ class QuizViewModel(
7379
}
7480

7581
private suspend fun runCountdown() {
76-
for (sec in QuizRules.INITIAL_COUNTDOWN_SECONDS downTo 1) {
82+
timer.runCountdown(QuizRules.INITIAL_COUNTDOWN_SECONDS) { sec ->
7783
_uiState.update { it.copy(phase = QuizPhase.Countdown(sec)) }
78-
delay(1.seconds)
7984
}
8085
_uiState.update { it.copy(phase = QuizPhase.Loading) }
8186
}
8287

8388
private fun startPlaying(question: Question) {
8489
_uiState.update { it.copy(phase = QuizPhase.Playing(question = question)) }
85-
startTimer()
90+
startQuestionTimer()
8691
schedulePrefetchOfNextQuestion()
8792
}
8893

89-
private fun startTimer() {
90-
timerJob?.cancel()
94+
private fun startQuestionTimer() {
9195
val mode = _uiState.value.challengeMode
92-
93-
if (mode !is ChallengeMode.Timed) {
94-
_uiState.update { it.copy(timeRemainingSec = null) }
95-
return
96+
if (mode is ChallengeMode.Timed) {
97+
timer.start(viewModelScope, mode.perQuestionSeconds) { onTimeOut() }
9698
}
97-
98-
val totalSeconds = mode.perQuestionSeconds
99-
timerJob =
100-
viewModelScope.launch {
101-
for (sec in totalSeconds downTo 1) {
102-
_uiState.update { it.copy(timeRemainingSec = sec) }
103-
delay(1.seconds)
104-
}
105-
_uiState.update { it.copy(timeRemainingSec = 0) }
106-
onTimeOut()
107-
}
10899
}
109100

110101
private fun onTimeOut() {
@@ -150,7 +141,7 @@ class QuizViewModel(
150141
playing: QuizPhase.Playing,
151142
answer: String,
152143
) {
153-
timerJob?.cancel()
144+
timer.cancel()
154145
_uiState.update {
155146
it.copy(phase = playing.copy(selectedAnswer = answer, isSubmitting = true))
156147
}
@@ -194,12 +185,12 @@ class QuizViewModel(
194185
finalIndex: Int,
195186
) {
196187
val mode = _uiState.value.challengeMode
188+
timer.stop()
197189
_uiState.update {
198190
it.copy(
199191
phase = QuizPhase.Completed,
200192
answerLog = answerLog.toImmutableList(),
201193
currentQuestionIndex = finalIndex,
202-
timeRemainingSec = null,
203194
)
204195
}
205196

@@ -248,7 +239,7 @@ class QuizViewModel(
248239
currentQuestionIndex = nextIndex,
249240
)
250241
}
251-
startTimer()
242+
startQuestionTimer()
252243
schedulePrefetchOfNextQuestion()
253244
}
254245

@@ -266,7 +257,7 @@ class QuizViewModel(
266257
private fun currentPlaying(): QuizPhase.Playing? = _uiState.value.phase as? QuizPhase.Playing
267258

268259
override fun onCleared() {
269-
timerJob?.cancel()
260+
timer.cancel()
270261
prefetchedNextQuestion?.cancel()
271262
super.onCleared()
272263
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
package com.leanite.dynaquiz.feature.quiz.di
22

3+
import com.leanite.dynaquiz.feature.quiz.QuizTimerController
34
import com.leanite.dynaquiz.feature.quiz.QuizViewModel
45
import org.koin.core.module.dsl.viewModel
56
import org.koin.dsl.module
67

78
val featureQuizModule =
89
module {
10+
factory { QuizTimerController() }
911
viewModel { params ->
1012
QuizViewModel(
1113
setup = params.get(),
1214
getRandomQuestionUseCase = get(),
1315
submitAnswerUseCase = get(),
1416
saveQuizSessionUseCase = get(),
17+
timer = get(),
1518
)
1619
}
1720
}

dynaquiz/composeApp/src/commonTest/kotlin/com/leanite/dynaquiz/feature/quiz/QuizViewModelTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class QuizViewModelTest {
7474
getRandomQuestionUseCase = getRandomQuestionUseCase,
7575
submitAnswerUseCase = submitAnswerUseCase,
7676
saveQuizSessionUseCase = saveQuizSessionUseCase,
77+
timer = QuizTimerController(),
7778
)
7879

7980
private fun question(

0 commit comments

Comments
 (0)