refactor(stats): refactor flow collection to improve performance

This commit is contained in:
Nishant Mishra
2025-10-15 23:25:29 +05:30
parent 5717a0f274
commit a26b0eac74
2 changed files with 128 additions and 93 deletions

View File

@@ -35,6 +35,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -56,10 +57,8 @@ import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
import com.patrykandpatrick.vico.core.common.data.ExtraStore import com.patrykandpatrick.vico.core.common.data.ExtraStore
import kotlinx.coroutines.runBlocking
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.data.Stat import org.nsh07.pomodoro.data.Stat
import org.nsh07.pomodoro.data.StatFocusTime
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
@@ -72,20 +71,41 @@ fun StatsScreenRoot(
viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory) viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
) { ) {
val todayStat by viewModel.todayStat.collectAsStateWithLifecycle(null) val todayStat by viewModel.todayStat.collectAsStateWithLifecycle(null)
val lastWeekAverageFocusTimes by viewModel
.lastWeekAverageFocusTimes.collectAsStateWithLifecycle(null) val lastWeekSummaryChartData by viewModel.lastWeekSummaryChartData.collectAsStateWithLifecycle()
val lastMonthAverageFocusTimes by viewModel val lastWeekAnalysisValues by viewModel.lastWeekAverageFocusTimes.collectAsStateWithLifecycle()
.lastMonthAverageFocusTimes.collectAsStateWithLifecycle(null)
val lastMonthSummaryChartData by viewModel.lastMonthSummaryChartData.collectAsStateWithLifecycle()
val lastMonthAnalysisValues by viewModel.lastMonthAverageFocusTimes.collectAsStateWithLifecycle()
val lastWeekSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() }
val lastMonthSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() }
LaunchedEffect(lastWeekAnalysisValues) {
lastWeekSummaryAnalysisModelProducer.runTransaction {
columnSeries {
series(lastWeekAnalysisValues)
}
}
}
LaunchedEffect(lastMonthAnalysisValues) {
lastMonthSummaryAnalysisModelProducer.runTransaction {
columnSeries {
series(lastMonthAnalysisValues)
}
}
}
StatsScreen( StatsScreen(
contentPadding = contentPadding, contentPadding = contentPadding,
lastWeekSummaryChartData = remember { viewModel.lastWeekSummaryChartData }, lastWeekSummaryChartData = lastWeekSummaryChartData,
lastWeekSummaryAnalysisModelProducer = remember { viewModel.lastWeekSummaryAnalysisModelProducer }, lastWeekSummaryAnalysisModelProducer = lastWeekSummaryAnalysisModelProducer,
lastMonthSummaryChartData = remember { viewModel.lastMonthSummaryChartData }, lastMonthSummaryChartData = lastMonthSummaryChartData,
lastMonthSummaryAnalysisModelProducer = remember { viewModel.lastMonthSummaryAnalysisModelProducer }, lastMonthSummaryAnalysisModelProducer = lastMonthSummaryAnalysisModelProducer,
todayStat = todayStat, todayStat = todayStat,
lastWeekAverageFocusTimes = lastWeekAverageFocusTimes, lastWeekAverageFocusTimes = lastWeekAnalysisValues,
lastMonthAverageFocusTimes = lastMonthAverageFocusTimes, lastMonthAverageFocusTimes = lastMonthAnalysisValues,
modifier = modifier modifier = modifier
) )
} }
@@ -99,8 +119,8 @@ fun StatsScreen(
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>, lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
lastMonthSummaryAnalysisModelProducer: CartesianChartModelProducer, lastMonthSummaryAnalysisModelProducer: CartesianChartModelProducer,
todayStat: Stat?, todayStat: Stat?,
lastWeekAverageFocusTimes: StatFocusTime?, lastWeekAverageFocusTimes: List<Int>,
lastMonthAverageFocusTimes: StatFocusTime?, lastMonthAverageFocusTimes: List<Int>,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
@@ -218,7 +238,11 @@ fun StatsScreen(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
Text( Text(
millisecondsToHoursMinutes(lastWeekAverageFocusTimes?.total() ?: 0), millisecondsToHoursMinutes(
remember(lastWeekAverageFocusTimes) {
lastWeekAverageFocusTimes.sum().toLong()
}
),
style = typography.displaySmall, style = typography.displaySmall,
fontFamily = openRundeClock fontFamily = openRundeClock
) )
@@ -290,7 +314,11 @@ fun StatsScreen(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
Text( Text(
millisecondsToHoursMinutes(lastMonthAverageFocusTimes?.total() ?: 0), millisecondsToHoursMinutes(
remember(lastMonthAverageFocusTimes) {
lastMonthAverageFocusTimes.sum().toLong()
}
),
style = typography.displaySmall, style = typography.displaySmall,
fontFamily = openRundeClock fontFamily = openRundeClock
) )
@@ -356,23 +384,25 @@ fun StatsScreen(
@Composable @Composable
fun StatsScreenPreview() { fun StatsScreenPreview() {
val modelProducer = remember { CartesianChartModelProducer() } val modelProducer = remember { CartesianChartModelProducer() }
val keys = remember { ExtraStore.Key<List<String>>() }
runBlocking { LaunchedEffect(Unit) {
modelProducer.runTransaction { modelProducer.runTransaction {
columnSeries { columnSeries {
series(5, 6, 5, 2, 11, 8, 5, 2, 15, 11, 8, 13, 12, 10, 2, 7) series(5, 6, 5, 2, 11, 8, 5, 2, 15, 11, 8, 13, 12, 10, 2, 7)
} }
extras { it[keys] = listOf("M", "T", "W", "T", "F", "S", "S") }
} }
} }
StatsScreen( StatsScreen(
PaddingValues(), PaddingValues(),
Pair(modelProducer, ExtraStore.Key()), Pair(modelProducer, keys),
modelProducer, modelProducer,
Pair(modelProducer, ExtraStore.Key()), Pair(modelProducer, keys),
modelProducer, modelProducer,
null, null,
null, listOf(0, 0, 0, 0),
null listOf(0, 0, 0, 0)
) )
} }

View File

@@ -16,9 +16,11 @@ import androidx.lifecycle.viewmodel.viewModelFactory
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
import com.patrykandpatrick.vico.core.common.data.ExtraStore import com.patrykandpatrick.vico.core.common.data.ExtraStore
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.data.StatRepository import org.nsh07.pomodoro.data.StatRepository
import java.time.format.TextStyle import java.time.format.TextStyle
@@ -29,83 +31,86 @@ class StatsViewModel(
) : ViewModel() { ) : ViewModel() {
val todayStat = statRepository.getTodayStat().distinctUntilChanged() val todayStat = statRepository.getTodayStat().distinctUntilChanged()
private val lastWeekStatsSummary = statRepository.getLastNDaysStatsSummary(7)
val lastWeekAverageFocusTimes =
statRepository.getLastNDaysAverageFocusTimes(7).distinctUntilChanged()
private val lastMonthStatsSummary = statRepository.getLastNDaysStatsSummary(30)
val lastMonthAverageFocusTimes =
statRepository.getLastNDaysAverageFocusTimes(30).distinctUntilChanged()
val lastWeekSummaryChartData = private val lastWeekSummary =
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>()) Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
val lastWeekSummaryAnalysisModelProducer = CartesianChartModelProducer()
val lastMonthSummaryChartData = private val lastMonthSummary =
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>()) Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
val lastMonthSummaryAnalysisModelProducer = CartesianChartModelProducer()
init { val lastWeekSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
viewModelScope.launch(Dispatchers.IO) { statRepository.getLastNDaysStatsSummary(7)
lastWeekStatsSummary .map { list ->
.collect { list -> // reversing is required because we need ascending order while the DB returns descending order
// reversing is required because we need ascending order while the DB returns descending order val reversed = list.reversed()
val reversed = list.reversed() val keys = reversed.map {
val keys = reversed.map { it.date.dayOfWeek.getDisplayName(
it.date.dayOfWeek.getDisplayName( TextStyle.NARROW,
TextStyle.NARROW, Locale.getDefault()
Locale.getDefault() )
)
}
val values = reversed.map { it.focusTime }
lastWeekSummaryChartData.first.runTransaction {
columnSeries { series(values) }
extras { it[lastWeekSummaryChartData.second] = keys }
}
} }
} val values = reversed.map { it.focusTime }
viewModelScope.launch(Dispatchers.IO) { lastWeekSummary.first.runTransaction {
lastWeekAverageFocusTimes columnSeries { series(values) }
.collect { extras { it[lastWeekSummary.second] = keys }
lastWeekSummaryAnalysisModelProducer.runTransaction {
columnSeries {
series(
it?.focusTimeQ1 ?: 0,
it?.focusTimeQ2 ?: 0,
it?.focusTimeQ3 ?: 0,
it?.focusTimeQ4 ?: 0
)
}
}
} }
} lastWeekSummary
viewModelScope.launch(Dispatchers.IO) { }
lastMonthStatsSummary .stateIn(
.collect { list -> scope = viewModelScope,
val reversed = list.reversed() started = SharingStarted.WhileSubscribed(5000),
val keys = reversed.map { it.date.dayOfMonth.toString() } initialValue = lastWeekSummary
val values = reversed.map { it.focusTime } )
lastMonthSummaryChartData.first.runTransaction {
columnSeries { series(values) } val lastWeekAverageFocusTimes: StateFlow<List<Int>> =
extras { it[lastMonthSummaryChartData.second] = keys } statRepository.getLastNDaysAverageFocusTimes(7)
} .map {
listOf(
it?.focusTimeQ1?.toInt() ?: 0,
it?.focusTimeQ2?.toInt() ?: 0,
it?.focusTimeQ3?.toInt() ?: 0,
it?.focusTimeQ4?.toInt() ?: 0
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = listOf(0, 0, 0, 0)
)
val lastMonthSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
statRepository.getLastNDaysStatsSummary(30)
.map { list ->
val reversed = list.reversed()
val keys = reversed.map { it.date.dayOfMonth.toString() }
val values = reversed.map { it.focusTime }
lastMonthSummary.first.runTransaction {
columnSeries { series(values) }
extras { it[lastMonthSummary.second] = keys }
} }
} lastMonthSummary
viewModelScope.launch(Dispatchers.IO) { }
lastMonthAverageFocusTimes .stateIn(
.collect { scope = viewModelScope,
lastMonthSummaryAnalysisModelProducer.runTransaction { started = SharingStarted.WhileSubscribed(5000),
columnSeries { initialValue = lastMonthSummary
series( )
it?.focusTimeQ1 ?: 0,
it?.focusTimeQ2 ?: 0, val lastMonthAverageFocusTimes: StateFlow<List<Int>> =
it?.focusTimeQ3 ?: 0, statRepository.getLastNDaysAverageFocusTimes(30)
it?.focusTimeQ4 ?: 0 .map {
) listOf(
} it?.focusTimeQ1?.toInt() ?: 0,
} it?.focusTimeQ2?.toInt() ?: 0,
} it?.focusTimeQ3?.toInt() ?: 0,
} it?.focusTimeQ4?.toInt() ?: 0
} )
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = listOf(0, 0, 0, 0)
)
companion object { companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {