From 41bc1accb1b2b5521b3e5acd3d1d9e74fed08019 Mon Sep 17 00:00:00 2001 From: Ankit Suda Date: Mon, 21 Nov 2022 02:05:31 +0530 Subject: [PATCH] Paginate and improve history list --- .../com/ankitsuda/rebound/buildSrc/Deps.kt | 4 +- modules/core-data/build.gradle | 1 + .../rebound/data/db/daos/WorkoutsDao.kt | 28 ++++ .../data/repositories/WorkoutsRepository.kt | 80 +++++++---- .../domain/entities/ExerciseLogEntry.kt | 4 + .../entities/WorkoutWithExtraInfoAlt.kt | 31 +++++ .../rebound/ui/history/HistoryScreen.kt | 129 ++++++++++-------- .../ui/history/HistoryScreenViewModel.kt | 44 +++++- .../ui/history/models/CountWithLocalDate.kt | 22 +++ 9 files changed, 256 insertions(+), 87 deletions(-) create mode 100644 modules/domain/src/main/java/com/ankitsuda/rebound/domain/entities/WorkoutWithExtraInfoAlt.kt create mode 100644 modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/models/CountWithLocalDate.kt diff --git a/buildSrc/src/main/java/com/ankitsuda/rebound/buildSrc/Deps.kt b/buildSrc/src/main/java/com/ankitsuda/rebound/buildSrc/Deps.kt index 33947dca..9dcfa35c 100644 --- a/buildSrc/src/main/java/com/ankitsuda/rebound/buildSrc/Deps.kt +++ b/buildSrc/src/main/java/com/ankitsuda/rebound/buildSrc/Deps.kt @@ -68,7 +68,7 @@ object Deps { const val liveData = "androidx.compose.runtime:runtime-livedata:$version" const val activity = "androidx.activity:activity-compose:$activityVersion" const val viewModels = "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0" - const val paging = "androidx.paging:paging-compose:1.0.0-alpha14" + const val paging = "androidx.paging:paging-compose:1.0.0-alpha17" // private const val lottieVersion = "4.2.0" // const val lottie = "com.airbnb.android:lottie-compose:$lottieVersion" @@ -127,7 +127,7 @@ object Deps { } object Paging { - private const val version = "3.1.0-beta01" + private const val version = "3.1.1" const val common = "androidx.paging:paging-common-ktx:$version" const val runtime = "androidx.paging:paging-runtime-ktx:$version" diff --git a/modules/core-data/build.gradle b/modules/core-data/build.gradle index 1e4181c4..7209bd3a 100644 --- a/modules/core-data/build.gradle +++ b/modules/core-data/build.gradle @@ -57,6 +57,7 @@ dependencies { api project(":modules:common-data") api project(":modules:common-ui-compose") + api Deps.Android.Room.paging api Deps.Kotlin.stdlib kapt Deps.Dagger.hiltCompiler diff --git a/modules/core-data/src/main/java/com/ankitsuda/rebound/data/db/daos/WorkoutsDao.kt b/modules/core-data/src/main/java/com/ankitsuda/rebound/data/db/daos/WorkoutsDao.kt index 6e773b09..2cce2761 100644 --- a/modules/core-data/src/main/java/com/ankitsuda/rebound/data/db/daos/WorkoutsDao.kt +++ b/modules/core-data/src/main/java/com/ankitsuda/rebound/data/db/daos/WorkoutsDao.kt @@ -14,6 +14,7 @@ package com.ankitsuda.rebound.data.db.daos +import androidx.paging.PagingSource import androidx.room.* import androidx.sqlite.db.SupportSQLiteQuery import com.ankitsuda.base.utils.generateId @@ -143,9 +144,20 @@ interface WorkoutsDao { @RawQuery(observedEntities = [Workout::class, ExerciseLogEntry::class, ExerciseWorkoutJunction::class]) fun getAllWorkoutsRawQuery(query: SupportSQLiteQuery): Flow> + @RawQuery(observedEntities = [Workout::class, ExerciseLogEntry::class, ExerciseWorkoutJunction::class]) + fun getAllWorkoutsRawQueryPaged(query: SupportSQLiteQuery): PagingSource + @Query("SELECT COUNT(*) as count, start_at as date FROM workouts WHERE date(start_at / 1000,'unixepoch') >= date(:dateStart / 1000,'unixepoch') AND date(start_at / 1000,'unixepoch') <= date(:dateEnd / 1000,'unixepoch') AND is_hidden = 0 AND in_progress = 0 GROUP BY start_at") fun getWorkoutsCountOnDateRange(dateStart: Long, dateEnd: Long): Flow> + @Query(""" + SELECT SUM(count) FROM (SELECT COUNT(*) as count FROM workouts WHERE +date(start_at / 10000,'unixepoch') >= date(:date / 10000,'unixepoch') AND + date(start_at / 10000,'unixepoch') <= date(:date / 10000,'unixepoch') +AND is_hidden = 0 AND in_progress = 0 GROUP BY start_at) + """) + fun getWorkoutsCountOnMonth(date: Long): Flow + @Query("SELECT * FROM exercise_logs WHERE id = :logId") fun getExerciseLogByLogId(logId: String): Flow @@ -186,6 +198,22 @@ interface WorkoutsDao { @Query("UPDATE exercise_workout_junctions SET superset_id = :supersetId WHERE id = :junctionId") suspend fun updateExerciseWorkoutJunctionSupersetId(junctionId: String, supersetId: Int?) + @Query( +// """ +// SELECT w.*, le.* FROM workouts w +// JOIN exercise_workout_junctions j ON j.workout_id = w.id +// JOIN exercise_log_entries le ON j.id = le.junction_id +// WHERE w.is_hidden = 0 AND w.in_progress = 0 +// ORDER BY w.completed_at DESC +// """ + """ + SELECT * FROM workouts w + WHERE w.is_hidden = 0 AND w.in_progress = 0 + ORDER BY w.completed_at DESC + """ + ) + fun getWorkoutsWithExtraInfoAltPaged(): PagingSource + @Transaction suspend fun updateWarmUpSets( junction: LogEntriesWithExerciseJunction, diff --git a/modules/core-data/src/main/java/com/ankitsuda/rebound/data/repositories/WorkoutsRepository.kt b/modules/core-data/src/main/java/com/ankitsuda/rebound/data/repositories/WorkoutsRepository.kt index 074a167f..307ade1f 100644 --- a/modules/core-data/src/main/java/com/ankitsuda/rebound/data/repositories/WorkoutsRepository.kt +++ b/modules/core-data/src/main/java/com/ankitsuda/rebound/data/repositories/WorkoutsRepository.kt @@ -14,6 +14,9 @@ package com.ankitsuda.rebound.data.repositories +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.map import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import com.ankitsuda.base.util.NONE_WORKOUT_ID @@ -226,6 +229,40 @@ class WorkoutsRepository @Inject constructor( it.calculateTotalVolume() } + private suspend fun getWorkoutExtraInfo(workout: Workout): WorkoutWithExtraInfo { + val totalVolume = + getTotalVolumeLiftedByWorkoutId(workoutId = workout.id).firstOrNull() + + val totalExercises = + getExercisesCountByWorkoutId(workoutId = workout.id).firstOrNull() + + var totalPRs = 0 + + val totalPRsOfEntries = + getPRsCountOfEntriesByWorkoutId(workoutId = workout.id).firstOrNull() + + totalPRs += workout.personalRecords?.size ?: 0 + totalPRs += totalPRsOfEntries ?: 0 + + + return WorkoutWithExtraInfo( + workout = workout, + totalVolume = totalVolume, + totalExercises = totalExercises, + totalPRs = totalPRs + ) + } + + private suspend fun getWorkoutsExtraInfo(workouts: List): List { + val mWorkouts = arrayListOf() + for (workout in workouts) { + mWorkouts.add( + getWorkoutExtraInfo(workout) + ) + } + + return mWorkouts.toList() + } fun getWorkoutsWithExtraInfo(date: LocalDate? = null) = workoutsDao.getAllWorkoutsRawQuery( @@ -234,41 +271,38 @@ class WorkoutsRepository @Inject constructor( if (date != null) arrayOf(date.toEpochMillis()) else arrayOf() ) ).map { - val mWorkouts = arrayListOf() - for (workout in it) { - val totalVolume = - getTotalVolumeLiftedByWorkoutId(workoutId = workout.id).firstOrNull() - - val totalExercises = - getExercisesCountByWorkoutId(workoutId = workout.id).firstOrNull() - - var totalPRs = 0 - - val totalPRsOfEntries = - getPRsCountOfEntriesByWorkoutId(workoutId = workout.id).firstOrNull() + getWorkoutsExtraInfo(it) + } - totalPRs += workout.personalRecords?.size ?: 0 - totalPRs += totalPRsOfEntries ?: 0 +// fun getWorkoutsWithExtraInfoAlt() = workoutsDao.getWorkoutsWithExtraInfoAlt() - mWorkouts.add( + fun getWorkoutsWithExtraInfoPaged() = + Pager(PagingConfig(pageSize = 15)) { + workoutsDao.getWorkoutsWithExtraInfoAltPaged() + } + .flow.map { + it.map { item -> + val logEntries = item.junctions?.flatMap { j -> j.logEntries } WorkoutWithExtraInfo( - workout = workout, - totalVolume = totalVolume, - totalExercises = totalExercises, - totalPRs = totalPRs + workout = item.workout, + totalVolume = logEntries?.calculateTotalVolume(), + totalExercises = item.junctions?.size, + totalPRs = logEntries?.getTotalPRs(item.workout?.personalRecords?.size) ) - ) + } } - mWorkouts.toList() - } - fun getWorkoutsCountOnDateRange(dateStart: LocalDate, dateEnd: LocalDate) = workoutsDao.getWorkoutsCountOnDateRange( dateStart = dateStart.toEpochMillis(), dateEnd = dateEnd.toEpochMillis() ) + fun getWorkoutsCountOnMonth(date: Long) = + workoutsDao.getWorkoutsCountOnMonth( + date = date + ) + fun getExerciseLogByLogId(logId: String) = workoutsDao.getExerciseLogByLogId(logId) private suspend fun checkIfWorkoutIsActive(discardActive: Boolean): Boolean { diff --git a/modules/domain/src/main/java/com/ankitsuda/rebound/domain/entities/ExerciseLogEntry.kt b/modules/domain/src/main/java/com/ankitsuda/rebound/domain/entities/ExerciseLogEntry.kt index 70159ac2..67c495d1 100644 --- a/modules/domain/src/main/java/com/ankitsuda/rebound/domain/entities/ExerciseLogEntry.kt +++ b/modules/domain/src/main/java/com/ankitsuda/rebound/domain/entities/ExerciseLogEntry.kt @@ -81,4 +81,8 @@ fun List.calculateTotalVolume(): Double { volume += ((entry.weight ?: 0.0) * (entry.reps ?: 0).toDouble()) } return volume +} + +fun List.getTotalPRs(workoutPrs: Int? = null): Int { + return sumOf { it.personalRecords?.size ?: 0 } + (workoutPrs ?: 0) } \ No newline at end of file diff --git a/modules/domain/src/main/java/com/ankitsuda/rebound/domain/entities/WorkoutWithExtraInfoAlt.kt b/modules/domain/src/main/java/com/ankitsuda/rebound/domain/entities/WorkoutWithExtraInfoAlt.kt new file mode 100644 index 00000000..0a48d49a --- /dev/null +++ b/modules/domain/src/main/java/com/ankitsuda/rebound/domain/entities/WorkoutWithExtraInfoAlt.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 Ankit Suda. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package com.ankitsuda.rebound.domain.entities + +import androidx.room.* +import com.ankitsuda.base.utils.toEpochMillis +import java.time.LocalDateTime +import java.util.* + +data class WorkoutWithExtraInfoAlt( + @Embedded + var workout: Workout? = null, + @Relation( + entity = ExerciseWorkoutJunction::class, + parentColumn = "id", + entityColumn = "workout_id" + ) + var junctions: List? = null, +) diff --git a/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/HistoryScreen.kt b/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/HistoryScreen.kt index 85c3a069..c4b2eb95 100644 --- a/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/HistoryScreen.kt +++ b/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/HistoryScreen.kt @@ -38,13 +38,19 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.items import com.ankitsuda.base.util.toReadableString +import com.ankitsuda.base.utils.generateId import com.ankitsuda.base.utils.toLocalDate import com.ankitsuda.navigation.* +import com.ankitsuda.rebound.domain.entities.CountWithDate +import com.ankitsuda.rebound.domain.entities.WorkoutWithExtraInfo import com.ankitsuda.rebound.ui.components.TopBar2 import com.ankitsuda.rebound.ui.components.TopBarIconButton import com.ankitsuda.rebound.ui.history.components.HistoryHeader import com.ankitsuda.rebound.ui.history.components.HistorySessionItemCard +import com.ankitsuda.rebound.ui.history.models.CountWithLocalDate import com.ankitsuda.rebound.ui.theme.ReboundTheme import kotlinx.coroutines.delay import me.onebone.toolbar.CollapsingToolbarScaffold @@ -69,38 +75,39 @@ fun HistoryScreen( val collapsingState = rememberCollapsingToolbarScaffoldState() - val workoutsMap by viewModel.workouts.collectAsState(initial = emptyMap()) +// val workoutsMap by viewModel.workouts.collectAsState(initial = emptyMap()) + val workoutsPage = viewModel.workouts.collectAsLazyPagingItems() - LaunchedEffect(key1 = argumentsDate?.value) { - if (argumentsDate?.value == null) return@LaunchedEffect; - - try { - val mDate = argumentsDate.value?.toLocalDate(); - var index = -1; - - var loopIndex = 0; - for (map in workoutsMap) { - if (map.key?.month == mDate?.month && map.key?.year == mDate?.year) { - index = loopIndex; - break; - } else { - loopIndex += 1 + map.value.size - } - } - - if (index > -1) { - delay(100) - scrollState.animateScrollToItem( - index - ) - } - - navController.currentBackStackEntry - ?.savedStateHandle?.remove(DATE_KEY) - } catch (e: Exception) { - e.printStackTrace() - } - } +// LaunchedEffect(key1 = argumentsDate?.value) { +// if (argumentsDate?.value == null) return@LaunchedEffect; +// +// try { +// val mDate = argumentsDate.value?.toLocalDate(); +// var index = -1; +// +// var loopIndex = 0; +// for (map in workoutsMap) { +// if (map.key?.month == mDate?.month && map.key?.year == mDate?.year) { +// index = loopIndex; +// break; +// } else { +// loopIndex += 1 + map.value.size +// } +// } +// +// if (index > -1) { +// delay(100) +// scrollState.animateScrollToItem( +// index +// ) +// } +// +// navController.currentBackStackEntry +// ?.savedStateHandle?.remove(DATE_KEY) +// } catch (e: Exception) { +// e.printStackTrace() +// } +// } CollapsingToolbarScaffold( scrollStrategy = ScrollStrategy.EnterAlwaysCollapsed, @@ -141,34 +148,44 @@ fun HistoryScreen( verticalArrangement = Arrangement.spacedBy(20.dp), contentPadding = PaddingValues(24.dp) ) { - for (map in workoutsMap) { - if (map.key != null) { - item(key = "${map.key}") { - HistoryHeader( - date = map.key!!, - totalWorkouts = map.value.size - ) + items(workoutsPage, key = { + when (it) { + is WorkoutWithExtraInfo -> { + it.workout!!.id + } + is CountWithDate -> { + it.date + } + else -> { + it } } - items(map.value, key = { it.workout!!.id }) { - HistorySessionItemCard( - modifier = Modifier - .fillMaxWidth(), - onClick = { - navigator.navigate( - LeafScreen.Session.createRoute( - workoutId = it.workout?.id!! + }) { + when (it) { + is WorkoutWithExtraInfo -> + HistorySessionItemCard( + modifier = Modifier + .fillMaxWidth(), + onClick = { + navigator.navigate( + LeafScreen.Session.createRoute( + workoutId = it.workout?.id!! + ) ) - ) - }, - title = it.workout?.name.toString(), - totalExercises = it.totalExercises ?: 0, - duration = it.workout?.getDuration(), - volume = it.totalVolume, - prs = it.totalPRs ?: 0, - date = it.workout?.startAt ?: it.workout?.completedAt - ?: it.workout?.createdAt, - ) + }, + title = it?.workout?.name.toString(), + totalExercises = it?.totalExercises ?: 0, + duration = it?.workout?.getDuration(), + volume = it?.totalVolume, + prs = it?.totalPRs ?: 0, + date = it?.workout?.startAt ?: it?.workout?.completedAt + ?: it?.workout?.createdAt, + ) + is CountWithDate -> + HistoryHeader( + date = it.date.toLocalDate() ?: LocalDate.now(), + totalWorkouts = it.count.toInt() + ) } } } diff --git a/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/HistoryScreenViewModel.kt b/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/HistoryScreenViewModel.kt index 568adaaa..fc285275 100644 --- a/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/HistoryScreenViewModel.kt +++ b/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/HistoryScreenViewModel.kt @@ -16,11 +16,17 @@ package com.ankitsuda.rebound.ui.history import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.insertSeparators import com.ankitsuda.base.utils.extensions.shareWhileObserved +import com.ankitsuda.base.utils.toEpochMillis import com.ankitsuda.rebound.data.repositories.WorkoutsRepository +import com.ankitsuda.rebound.domain.entities.CountWithDate +import com.ankitsuda.rebound.domain.entities.WorkoutWithExtraInfo import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.map -import java.time.LocalDate +import kotlinx.coroutines.flow.* +import timber.log.Timber import java.time.temporal.TemporalAdjusters import javax.inject.Inject @@ -28,11 +34,37 @@ import javax.inject.Inject class HistoryScreenViewModel @Inject constructor( private val workoutsRepository: WorkoutsRepository, ) : ViewModel() { - val workouts = workoutsRepository.getWorkoutsWithExtraInfo() + val workouts = workoutsRepository.getWorkoutsWithExtraInfoPaged() .map { - it.groupBy { w -> - w.workout?.completedAt?.toLocalDate()?.with(TemporalAdjusters.firstDayOfMonth()) + mapData(it) + } + .cachedIn(viewModelScope) + .shareWhileObserved(viewModelScope) + + private fun mapData(pagingData: PagingData) = + pagingData.insertSeparators { before, after -> + if (after != null) { + val afterDate = after.workout?.completedAt?.toLocalDate() + ?.with(TemporalAdjusters.firstDayOfMonth()) + + val beforeDate = before?.workout?.completedAt?.toLocalDate() + ?.with(TemporalAdjusters.firstDayOfMonth()) + + if (after.workout?.completedAt != null && afterDate != null && beforeDate != afterDate) { + val mWorkoutsCounts = workoutsRepository.getWorkoutsCountOnMonth( + date = after.workout!!.completedAt!!.toEpochMillis() + ).firstOrNull() + + CountWithDate( + date = afterDate.toEpochMillis(), + count = mWorkoutsCounts ?: 0 + ) + } else { + null + } + } else { + null } } - .shareWhileObserved(viewModelScope); + } \ No newline at end of file diff --git a/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/models/CountWithLocalDate.kt b/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/models/CountWithLocalDate.kt new file mode 100644 index 00000000..8c75a73e --- /dev/null +++ b/modules/ui-history/src/main/java/com/ankitsuda/rebound/ui/history/models/CountWithLocalDate.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 Ankit Suda. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package com.ankitsuda.rebound.ui.history.models + +import java.time.LocalDate + +internal data class CountWithLocalDate( + val count: Long, + val date: LocalDate +) \ No newline at end of file