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")
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?>
@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
import androidx.compose.animation.ContentTransform
@@ -22,10 +29,8 @@ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
@@ -38,9 +43,6 @@ import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
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.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.StatsScreen
@@ -57,20 +59,12 @@ fun AppScreen(
val remainingTime by viewModel.time.collectAsStateWithLifecycle()
val progress by rememberUpdatedState((uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime)
var showBrandTitle by remember { mutableStateOf(true) }
val layoutDirection = LocalLayoutDirection.current
val haptic = LocalHapticFeedback.current
val motionScheme = motionScheme
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
delay(1500)
showBrandTitle = false
}
}
LaunchedEffect(uiState.timerMode) {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
@@ -142,11 +136,8 @@ fun AppScreen(
entry<Screen.Timer> {
TimerScreen(
timerState = uiState,
showBrandTitle = showBrandTitle,
progress = { progress },
resetTimer = viewModel::resetTimer,
skipTimer = viewModel::skipTimer,
toggleTimer = viewModel::toggleTimer,
onAction = viewModel::onAction,
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(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
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.robotoFlexTitle
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.TimerState
@@ -60,11 +68,8 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
@Composable
fun TimerScreen(
timerState: TimerState,
showBrandTitle: Boolean,
progress: () -> Float,
resetTimer: () -> Unit,
skipTimer: () -> Unit,
toggleTimer: () -> Unit,
onAction: (TimerAction) -> Unit,
modifier: Modifier = Modifier
) {
val motionScheme = motionScheme
@@ -89,7 +94,7 @@ fun TimerScreen(
TopAppBar(
title = {
AnimatedContent(
if (!showBrandTitle) timerState.timerMode else TimerMode.BRAND,
if (!timerState.showBrandTitle) timerState.timerMode else TimerMode.BRAND,
transitionSpec = {
slideInVertically(
animationSpec = motionScheme.slowSpatialSpec(),
@@ -245,7 +250,7 @@ fun TimerScreen(
customItem(
{
FilledIconToggleButton(
onCheckedChange = { toggleTimer() },
onCheckedChange = { onAction(TimerAction.ToggleTimer) },
checked = timerState.timerRunning,
colors = IconButtonDefaults.filledIconToggleButtonColors(
checkedContainerColor = color,
@@ -289,7 +294,7 @@ fun TimerScreen(
},
text = { Text(if (timerState.timerRunning) "Pause" else "Play") },
onClick = {
toggleTimer()
onAction(TimerAction.ToggleTimer)
state.dismiss()
}
)
@@ -299,7 +304,7 @@ fun TimerScreen(
customItem(
{
FilledTonalIconButton(
onClick = resetTimer,
onClick = { onAction(TimerAction.ResetTimer) },
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = colorContainer
),
@@ -326,7 +331,7 @@ fun TimerScreen(
},
text = { Text("Restart") },
onClick = {
resetTimer()
onAction(TimerAction.ResetTimer)
state.dismiss()
}
)
@@ -336,7 +341,7 @@ fun TimerScreen(
customItem(
{
FilledTonalIconButton(
onClick = skipTimer,
onClick = { onAction(TimerAction.SkipTimer) },
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = colorContainer
),
@@ -363,7 +368,7 @@ fun TimerScreen(
},
text = { Text("Skip to next") },
onClick = {
skipTimer()
onAction(TimerAction.SkipTimer)
state.dismiss()
}
)
@@ -411,10 +416,7 @@ fun TimerScreenPreview() {
TomatoTheme {
TimerScreen(
timerState,
false,
{ 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 timerRunning: Boolean = false,
val nextTimerMode: TimerMode = TimerMode.SHORT_BREAK,
val nextTimeStr: String = "5:00"
val nextTimeStr: String = "5:00",
val showBrandTitle: Boolean = true
)
enum class TimerMode {

View File

@@ -82,10 +82,24 @@ class TimerViewModel(
)
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 {
saveTimeToDb()
_time.update { timerRepository.focusTime }
@@ -106,7 +120,7 @@ class TimerViewModel(
}
}
fun skipTimer() {
private fun skipTimer() {
viewModelScope.launch {
saveTimeToDb()
startTime = 0L
@@ -147,7 +161,7 @@ class TimerViewModel(
}
}
fun toggleTimer() {
private fun toggleTimer() {
if (timerState.value.timerRunning) {
_timerState.update { currentState ->
currentState.copy(timerRunning = false)