refactor: Migrate TimerViewModel from MVVM to MVI

Also fix a bug in the StatDao that prevented app from compiling
This commit is contained in:
Nishant Mishra
2025-07-10 20:04:24 +05:30
parent 66d70a9683
commit 10d6e4e293
6 changed files with 58 additions and 36 deletions

View File

@@ -39,7 +39,7 @@ interface StatDao {
@Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat") @Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat")
fun getAllStatsSummary(): Flow<List<StatSummary>> fun getAllStatsSummary(): Flow<List<StatSummary>>
@Query("SELECT AVG(focusTimeQ1), AVG(focusTimeQ2), AVG(focusTimeQ3), AVG(focusTimeQ4) FROM stat") @Query("SELECT AVG(focusTimeQ1) AS focusTimeQ1, AVG(focusTimeQ2) AS focusTimeQ2, AVG(focusTimeQ3) AS focusTimeQ3, AVG(focusTimeQ4) AS focusTimeQ4 FROM stat")
fun getAvgFocusTimes(): Flow<StatFocusTime?> fun getAvgFocusTimes(): Flow<StatFocusTime?>
@Query("SELECT EXISTS (SELECT * FROM stat WHERE date = :date)") @Query("SELECT EXISTS (SELECT * FROM stat WHERE date = :date)")

View File

@@ -1,3 +1,10 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui package org.nsh07.pomodoro.ui
import androidx.compose.animation.ContentTransform import androidx.compose.animation.ContentTransform
@@ -22,10 +29,8 @@ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
@@ -38,9 +43,6 @@ import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import androidx.window.core.layout.WindowSizeClass import androidx.window.core.layout.WindowSizeClass
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.nsh07.pomodoro.MainActivity.Companion.screens import org.nsh07.pomodoro.MainActivity.Companion.screens
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.StatsScreen import org.nsh07.pomodoro.ui.statsScreen.StatsScreen
@@ -57,20 +59,12 @@ fun AppScreen(
val remainingTime by viewModel.time.collectAsStateWithLifecycle() val remainingTime by viewModel.time.collectAsStateWithLifecycle()
val progress by rememberUpdatedState((uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime) val progress by rememberUpdatedState((uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime)
var showBrandTitle by remember { mutableStateOf(true) }
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val motionScheme = motionScheme val motionScheme = motionScheme
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
delay(1500)
showBrandTitle = false
}
}
LaunchedEffect(uiState.timerMode) { LaunchedEffect(uiState.timerMode) {
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
} }
@@ -142,11 +136,8 @@ fun AppScreen(
entry<Screen.Timer> { entry<Screen.Timer> {
TimerScreen( TimerScreen(
timerState = uiState, timerState = uiState,
showBrandTitle = showBrandTitle,
progress = { progress }, progress = { progress },
resetTimer = viewModel::resetTimer, onAction = viewModel::onAction,
skipTimer = viewModel::skipTimer,
toggleTimer = viewModel::toggleTimer,
modifier = modifier.padding( modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection), start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection), end = contentPadding.calculateEndPadding(layoutDirection),

View File

@@ -1,3 +1,10 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.timerScreen package org.nsh07.pomodoro.ui.timerScreen
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
@@ -53,6 +60,7 @@ import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTitle import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTitle
import org.nsh07.pomodoro.ui.theme.TomatoTheme import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
@@ -60,11 +68,8 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
@Composable @Composable
fun TimerScreen( fun TimerScreen(
timerState: TimerState, timerState: TimerState,
showBrandTitle: Boolean,
progress: () -> Float, progress: () -> Float,
resetTimer: () -> Unit, onAction: (TimerAction) -> Unit,
skipTimer: () -> Unit,
toggleTimer: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val motionScheme = motionScheme val motionScheme = motionScheme
@@ -89,7 +94,7 @@ fun TimerScreen(
TopAppBar( TopAppBar(
title = { title = {
AnimatedContent( AnimatedContent(
if (!showBrandTitle) timerState.timerMode else TimerMode.BRAND, if (!timerState.showBrandTitle) timerState.timerMode else TimerMode.BRAND,
transitionSpec = { transitionSpec = {
slideInVertically( slideInVertically(
animationSpec = motionScheme.slowSpatialSpec(), animationSpec = motionScheme.slowSpatialSpec(),
@@ -245,7 +250,7 @@ fun TimerScreen(
customItem( customItem(
{ {
FilledIconToggleButton( FilledIconToggleButton(
onCheckedChange = { toggleTimer() }, onCheckedChange = { onAction(TimerAction.ToggleTimer) },
checked = timerState.timerRunning, checked = timerState.timerRunning,
colors = IconButtonDefaults.filledIconToggleButtonColors( colors = IconButtonDefaults.filledIconToggleButtonColors(
checkedContainerColor = color, checkedContainerColor = color,
@@ -289,7 +294,7 @@ fun TimerScreen(
}, },
text = { Text(if (timerState.timerRunning) "Pause" else "Play") }, text = { Text(if (timerState.timerRunning) "Pause" else "Play") },
onClick = { onClick = {
toggleTimer() onAction(TimerAction.ToggleTimer)
state.dismiss() state.dismiss()
} }
) )
@@ -299,7 +304,7 @@ fun TimerScreen(
customItem( customItem(
{ {
FilledTonalIconButton( FilledTonalIconButton(
onClick = resetTimer, onClick = { onAction(TimerAction.ResetTimer) },
colors = IconButtonDefaults.filledTonalIconButtonColors( colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = colorContainer containerColor = colorContainer
), ),
@@ -326,7 +331,7 @@ fun TimerScreen(
}, },
text = { Text("Restart") }, text = { Text("Restart") },
onClick = { onClick = {
resetTimer() onAction(TimerAction.ResetTimer)
state.dismiss() state.dismiss()
} }
) )
@@ -336,7 +341,7 @@ fun TimerScreen(
customItem( customItem(
{ {
FilledTonalIconButton( FilledTonalIconButton(
onClick = skipTimer, onClick = { onAction(TimerAction.SkipTimer) },
colors = IconButtonDefaults.filledTonalIconButtonColors( colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = colorContainer containerColor = colorContainer
), ),
@@ -363,7 +368,7 @@ fun TimerScreen(
}, },
text = { Text("Skip to next") }, text = { Text("Skip to next") },
onClick = { onClick = {
skipTimer() onAction(TimerAction.SkipTimer)
state.dismiss() state.dismiss()
} }
) )
@@ -411,10 +416,7 @@ fun TimerScreenPreview() {
TomatoTheme { TomatoTheme {
TimerScreen( TimerScreen(
timerState, timerState,
false,
{ 0.3f }, { 0.3f },
{},
{},
{} {}
) )
} }

View File

@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.timerScreen.viewModel
sealed interface TimerAction {
data object ResetTimer : TimerAction
data object SkipTimer : TimerAction
data object ToggleTimer : TimerAction
}

View File

@@ -13,7 +13,8 @@ data class TimerState(
val totalTime: Long = 25 * 60, val totalTime: Long = 25 * 60,
val timerRunning: Boolean = false, val timerRunning: Boolean = false,
val nextTimerMode: TimerMode = TimerMode.SHORT_BREAK, val nextTimerMode: TimerMode = TimerMode.SHORT_BREAK,
val nextTimeStr: String = "5:00" val nextTimeStr: String = "5:00",
val showBrandTitle: Boolean = true
) )
enum class TimerMode { enum class TimerMode {

View File

@@ -82,10 +82,24 @@ class TimerViewModel(
) )
resetTimer() resetTimer()
delay(1500)
_timerState.update { currentState ->
currentState.copy(showBrandTitle = false)
}
} }
} }
fun resetTimer() { fun onAction(action: TimerAction) {
when (action) {
TimerAction.ResetTimer -> resetTimer()
TimerAction.SkipTimer -> skipTimer()
TimerAction.ToggleTimer -> toggleTimer()
}
}
private fun resetTimer() {
viewModelScope.launch { viewModelScope.launch {
saveTimeToDb() saveTimeToDb()
_time.update { timerRepository.focusTime } _time.update { timerRepository.focusTime }
@@ -106,7 +120,7 @@ class TimerViewModel(
} }
} }
fun skipTimer() { private fun skipTimer() {
viewModelScope.launch { viewModelScope.launch {
saveTimeToDb() saveTimeToDb()
startTime = 0L startTime = 0L
@@ -147,7 +161,7 @@ class TimerViewModel(
} }
} }
fun toggleTimer() { private fun toggleTimer() {
if (timerState.value.timerRunning) { if (timerState.value.timerRunning) {
_timerState.update { currentState -> _timerState.update { currentState ->
currentState.copy(timerRunning = false) currentState.copy(timerRunning = false)