From 8d18775a889fdd5e615ed91563e491db6550c0c2 Mon Sep 17 00:00:00 2001 From: NiazSagor Date: Sun, 24 May 2026 21:14:36 +0600 Subject: [PATCH 1/4] Refactor HomeScreen to consume granular state flows and remove UserDetailsUiState - Remove the monolithic `UserDetailsUiState` data class and the associated `combine` logic in `HomeScreenViewModel`. - Expose individual UI states from `HomeScreenViewModel` as separate `StateFlow` properties with private backing fields. - Decompose `HomeScreen.kt` by introducing `HomeTopBar` and `HomeContent` components to improve modularity and state handling. - Refactor `UserProfileContent` to accept individual state parameters instead of a single UI state object. - Update `PreviewUserDetails` to reflect the updated component signatures and state structure. --- .../plus/ui/screens/home/HomeScreen.kt | 305 ++++++++++-------- .../ui/screens/home/HomeScreenViewModel.kt | 88 ++--- .../screens/home/model/UserDetailsUiState.kt | 19 -- 3 files changed, 202 insertions(+), 210 deletions(-) diff --git a/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreen.kt index 5806579..046929b 100644 --- a/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreen.kt @@ -107,7 +107,6 @@ import com.byteutility.dev.leetcode.plus.network.responseVo.Contest import com.byteutility.dev.leetcode.plus.ui.common.ProgressIndicator import com.byteutility.dev.leetcode.plus.ui.model.YouTubeVideo import com.byteutility.dev.leetcode.plus.ui.screens.home.model.DifficultyStatistics -import com.byteutility.dev.leetcode.plus.ui.screens.home.model.UserDetailsUiState import com.byteutility.dev.leetcode.plus.ui.screens.home.model.VideosByPlayListState import com.byteutility.dev.leetcode.plus.ui.theme.EasyText import com.byteutility.dev.leetcode.plus.ui.theme.HardText @@ -139,17 +138,12 @@ fun HomeScreen( onLogout: () -> Unit = {} ) { val viewModel: HomeScreenViewModel = hiltViewModel() - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val dailyProblem by viewModel.dailyProblem.collectAsStateWithLifecycle() - val dailyProblemSolved by viewModel.dailyProblemSolved.collectAsStateWithLifecycle() LifecycleResumeEffect(Unit) { viewModel.refreshUiState() onPauseOrDispose { } } HomeLayout( - uiState = uiState, - dailyProblem = dailyProblem, - dailyProblemSolved = dailyProblemSolved, + viewModel = viewModel, onSetGoal = onSetGoal, onGoalStatus = onGoalStatus, onTroubleShoot = onTroubleShoot, @@ -179,9 +173,7 @@ fun HomeScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeLayout( - uiState: UserDetailsUiState, - dailyProblem: LeetCodeProblem, - dailyProblemSolved: Boolean, + viewModel: HomeScreenViewModel, onSetGoal: () -> Unit, onGoalStatus: () -> Unit, onTroubleShoot: () -> Unit, @@ -213,49 +205,17 @@ fun HomeLayout( Scaffold( topBar = { - TopAppBar( - title = { - /** - * 5 times click in a shorter period will open troubleshoot page - */ - Text( - text = "Home", - fontWeight = FontWeight.Bold, - modifier = Modifier.clickable { - val currentTime = System.currentTimeMillis() - if (currentTime - lastClickTime <= 1000) { - clickCount++ - if (clickCount == 5) { - onTroubleShoot.invoke() - clickCount = 0 - } - } else { - clickCount = 1 - } - lastClickTime = currentTime - scope.launch { - delay(2000) - clickCount = 0 - } - }) - }, - actions = { - MainTopActions( - isWeeklyGoalSet = uiState.isWeeklyGoalSet, - avatarUrl = uiState.userBasicInfo.avatar, - onSetGoal = onSetGoal, - onGoalStatus = onGoalStatus, - onLogoutClick = { - showLogoutDialog = true - }, - modifier = Modifier.testTag("main_top_actions") - ) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = Color(0xFFABDEF5).copy( - alpha = 0.1f - ) - ) + HomeTopBar( + viewModel = viewModel, + clickCount = clickCount, + lastClickTime = lastClickTime, + onClickCountChange = { clickCount = it }, + onLastClickTimeChange = { lastClickTime = it }, + scope = scope, + onSetGoal = onSetGoal, + onGoalStatus = onGoalStatus, + onTroubleShoot = onTroubleShoot, + onLogoutClick = { showLogoutDialog = true } ) } ) { paddingValues -> @@ -269,10 +229,8 @@ fun HomeLayout( .fillMaxWidth() .weight(1f) ) { - UserProfileContent( - uiState = uiState, - dailyProblem = dailyProblem, - dailyProblemSolved = dailyProblemSolved, + HomeContent( + viewModel = viewModel, onNavigateToProblemDetails = onNavigateToProblemDetails, onLoadMoreSubmission = onLoadMoreSubmission, onLoadMoreVideos = onLoadMoreVideos, @@ -334,6 +292,64 @@ fun HomeLayout( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun HomeTopBar( + viewModel: HomeScreenViewModel, + clickCount: Int, + lastClickTime: Long, + onClickCountChange: (Int) -> Unit, + onLastClickTimeChange: (Long) -> Unit, + scope: kotlinx.coroutines.CoroutineScope, + onSetGoal: () -> Unit, + onGoalStatus: () -> Unit, + onTroubleShoot: () -> Unit, + onLogoutClick: () -> Unit +) { + val isWeeklyGoalSet by viewModel.isWeeklyGoalSet.collectAsStateWithLifecycle() + val userBasicInfo by viewModel.userBasicInfo.collectAsStateWithLifecycle() + + TopAppBar( + title = { + Text( + text = "Home", + fontWeight = FontWeight.Bold, + modifier = Modifier.clickable { + val currentTime = System.currentTimeMillis() + if (currentTime - lastClickTime <= 1000) { + val nextClickCount = clickCount + 1 + onClickCountChange(nextClickCount) + if (nextClickCount == 5) { + onTroubleShoot.invoke() + onClickCountChange(0) + } + } else { + onClickCountChange(1) + } + onLastClickTimeChange(currentTime) + scope.launch { + delay(2000) + onClickCountChange(0) + } + } + ) + }, + actions = { + MainTopActions( + isWeeklyGoalSet = isWeeklyGoalSet, + avatarUrl = userBasicInfo.avatar, + onSetGoal = onSetGoal, + onGoalStatus = onGoalStatus, + onLogoutClick = onLogoutClick, + modifier = Modifier.testTag("main_top_actions") + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color(0xFFABDEF5).copy(alpha = 0.1f) + ) + ) +} + @Composable fun MainTopActions( isWeeklyGoalSet: Boolean, @@ -364,9 +380,61 @@ fun MainTopActions( } } +@Composable +private fun HomeContent( + viewModel: HomeScreenViewModel, + onNavigateToProblemDetails: (String) -> Unit, + onLoadMoreSubmission: () -> Unit, + onLoadMoreVideos: () -> Unit, + onSearchClick: () -> Unit, + onSetInAppReminder: (Contest) -> Unit, + checkInAppContestReminderStatus: suspend (Contest) -> Boolean, + onNavigateToContestDetail: (Contest) -> Unit = {}, + modifier: Modifier = Modifier, +) { + val userBasicInfo by viewModel.userBasicInfo.collectAsStateWithLifecycle() + val syncInterval by viewModel.syncInterval.collectAsStateWithLifecycle() + val userContestInfo by viewModel.userContestInfo.collectAsStateWithLifecycle() + val userProblemSolvedInfo by viewModel.userProblemSolvedInfo.collectAsStateWithLifecycle() + val userSubmissionState by viewModel.userSubmissionState.collectAsStateWithLifecycle() + val videosByPlayListState by viewModel.videosByPlayListState.collectAsStateWithLifecycle() + val leetcodeUpcomingContestsState by viewModel.leetcodeUpcomingContestsState.collectAsStateWithLifecycle() + val difficultyStat by viewModel.difficultyStat.collectAsStateWithLifecycle() + val dailyProblem by viewModel.dailyProblem.collectAsStateWithLifecycle() + val dailyProblemSolved by viewModel.dailyProblemSolved.collectAsStateWithLifecycle() + + UserProfileContent( + userBasicInfo = userBasicInfo, + syncInterval = syncInterval, + userContestInfo = userContestInfo, + userProblemSolvedInfo = userProblemSolvedInfo, + userSubmissionState = userSubmissionState, + videosByPlayListState = videosByPlayListState, + leetcodeUpcomingContestsState = leetcodeUpcomingContestsState, + difficultyStat = difficultyStat, + dailyProblem = dailyProblem, + dailyProblemSolved = dailyProblemSolved, + onNavigateToProblemDetails = onNavigateToProblemDetails, + onLoadMoreSubmission = onLoadMoreSubmission, + onLoadMoreVideos = onLoadMoreVideos, + onSearchClick = onSearchClick, + onSetInAppReminder = onSetInAppReminder, + checkInAppContestReminderStatus = checkInAppContestReminderStatus, + onNavigateToContestDetail = onNavigateToContestDetail, + modifier = modifier + ) +} + @Composable fun UserProfileContent( - uiState: UserDetailsUiState, + userBasicInfo: UserBasicInfo, + syncInterval: Long, + userContestInfo: UserContestInfo, + userProblemSolvedInfo: UserProblemSolvedInfo, + userSubmissionState: com.byteutility.dev.leetcode.plus.ui.screens.home.model.UserSubmissionState, + videosByPlayListState: VideosByPlayListState, + leetcodeUpcomingContestsState: com.byteutility.dev.leetcode.plus.ui.screens.home.model.LeetcodeUpcomingContestsState, + difficultyStat: DifficultyStatistics, dailyProblem: LeetCodeProblem, dailyProblemSolved: Boolean, onNavigateToProblemDetails: (String) -> Unit, @@ -390,7 +458,7 @@ fun UserProfileContent( .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(10.dp) ) { - UserProfileCard(uiState.userBasicInfo) + UserProfileCard(userBasicInfo) // Data sync info message Row( @@ -408,7 +476,7 @@ fun UserProfileContent( ) Spacer(modifier = Modifier.width(6.dp)) Text( - text = "Data updated every ${uiState.syncInterval} min", + text = "Data updated every $syncInterval min", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 12.sp @@ -422,9 +490,9 @@ fun UserProfileContent( difficulty = dailyProblem.difficulty, onNavigateToProblemDetails = onNavigateToProblemDetails ) - UserStatisticsCard(uiState.userContestInfo) + UserStatisticsCard(userContestInfo) YouTubeVideoRowContent( - uiState.videosByPlayListState, + videosByPlayListState, onLoadMoreVideos, onSearchClick ) @@ -434,14 +502,17 @@ fun UserProfileContent( modifier = Modifier.padding(start = 8.dp) ) AutoScrollingContestList( - contests = uiState.leetcodeUpcomingContestsState.contests, + contests = leetcodeUpcomingContestsState.contests, onSetInAppReminder = onSetInAppReminder, checkInAppContestReminderStatus = checkInAppContestReminderStatus, onNavigateToContestDetail = onNavigateToContestDetail ) - UserProblemCategoryStats(userProblemSolvedInfo = uiState.userProblemSolvedInfo, diffStat = uiState.difficultyStat) + UserProblemCategoryStats( + userProblemSolvedInfo = userProblemSolvedInfo, + diffStat = difficultyStat + ) - if (uiState.userSubmissionState.submissions.isEmpty()) { + if (userSubmissionState.submissions.isEmpty()) { Text( text = "You have no recent submissions", modifier = Modifier.fillMaxWidth(), @@ -460,9 +531,9 @@ fun UserProfileContent( } } - items(uiState.userSubmissionState.submissions.size) { index -> - val item = uiState.userSubmissionState.submissions[index] - if (index >= uiState.userSubmissionState.submissions.size - 1 && !uiState.userSubmissionState.endReached && !uiState.userSubmissionState.isLoading) { + items(userSubmissionState.submissions.size) { index -> + val item = userSubmissionState.submissions[index] + if (index >= userSubmissionState.submissions.size - 1 && !userSubmissionState.endReached && !userSubmissionState.isLoading) { onLoadMoreSubmission() } SubmissionItem( @@ -472,7 +543,7 @@ fun UserProfileContent( } item { - if (uiState.userSubmissionState.isLoading) { + if (userSubmissionState.isLoading) { Row( modifier = Modifier .fillMaxWidth() @@ -1412,80 +1483,44 @@ private fun calculateRemainingTime(): String { @Preview(showBackground = true) @Composable fun PreviewUserDetails() { - val submissions = listOf( - UserSubmission( - lang = "volumus", - statusDisplay = "veri", - timestamp = "eu", - title = "reformidans" + UserProfileContent( + userBasicInfo = UserBasicInfo( + name = "Mindy Shannon", + userName = "Annette Jones", + avatar = "venenatis", + ranking = 8869, + country = "Gambia, The" ), - UserSubmission( - lang = "volumus", - statusDisplay = "veri", - timestamp = "eu", - title = "reformidans" + syncInterval = 30L, + userContestInfo = UserContestInfo( + rating = 14.15, + globalRanking = 3679, + attend = 7232 ), - UserSubmission( - lang = "volumus", - statusDisplay = "veri", - timestamp = "eu", - title = "reformidans" + userProblemSolvedInfo = UserProblemSolvedInfo( + easy = 4592, + medium = 5761, + hard = 6990 ), - UserSubmission( - lang = "volumus", - statusDisplay = "veri", - timestamp = "eu", - title = "reformidans" - ), - UserSubmission( - lang = "volumus", - statusDisplay = "veri", - timestamp = "eu", - title = "reformidans" - ), - UserSubmission( - lang = "volumus", - statusDisplay = "veri", - timestamp = "eu", - title = "reformidans" - ), - UserSubmission( - lang = "volumus", - statusDisplay = "veri", - timestamp = "eu", - title = "reformidans" - ), - ) - HomeLayout( - uiState = UserDetailsUiState( - userBasicInfo = UserBasicInfo( - name = "Mindy Shannon", - userName = "Annette Jones", - avatar = "venenatis", - ranking = 8869, - country = "Gambia, The" - ), - userContestInfo = UserContestInfo( - rating = 14.15, - globalRanking = 3679, - attend = 7232 - ), - userProblemSolvedInfo = UserProblemSolvedInfo( - easy = 4592, - medium = 5761, - hard = 6990 - ), + userSubmissionState = com.byteutility.dev.leetcode.plus.ui.screens.home.model.UserSubmissionState( + submissions = List(7) { + UserSubmission( + lang = "Kotlin", + statusDisplay = "Accepted", + timestamp = "Today", + title = "reformidans" + ) + } ), - LeetCodeProblem("Two Sum", "", ""), - false, - onSetGoal = {}, - onGoalStatus = {}, - onTroubleShoot = {}, + videosByPlayListState = VideosByPlayListState(), + leetcodeUpcomingContestsState = com.byteutility.dev.leetcode.plus.ui.screens.home.model.LeetcodeUpcomingContestsState(), + difficultyStat = DifficultyStatistics(), + dailyProblem = LeetCodeProblem("Two Sum", "", ""), + dailyProblemSolved = false, onNavigateToProblemDetails = {}, onLoadMoreSubmission = {}, onLoadMoreVideos = {}, onSearchClick = {}, - onLogout = {}, onSetInAppReminder = {}, checkInAppContestReminderStatus = { false }, onNavigateToContestDetail = {} diff --git a/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreenViewModel.kt b/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreenViewModel.kt index 32f44a2..fb9f79f 100644 --- a/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreenViewModel.kt +++ b/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreenViewModel.kt @@ -30,7 +30,6 @@ import com.byteutility.dev.leetcode.plus.network.responseVo.Contest import com.byteutility.dev.leetcode.plus.network.responseVo.sortByStartTime import com.byteutility.dev.leetcode.plus.ui.screens.home.model.DifficultyStatistics import com.byteutility.dev.leetcode.plus.ui.screens.home.model.LeetcodeUpcomingContestsState -import com.byteutility.dev.leetcode.plus.ui.screens.home.model.UserDetailsUiState import com.byteutility.dev.leetcode.plus.ui.screens.home.model.UserSubmissionState import com.byteutility.dev.leetcode.plus.ui.screens.home.model.VideosByPlayListState import dagger.hilt.android.lifecycle.HiltViewModel @@ -40,7 +39,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -64,36 +62,45 @@ class HomeScreenViewModel @Inject constructor( ) : ViewModel() { // Submissions - private val userSubmissionState = + private val _userSubmissionState = MutableStateFlow(UserSubmissionState()) + val userSubmissionState = _userSubmissionState.asStateFlow() private val userSubmissionPagination = getUserSubmissionPaginator() // Videos - private val videosByPlayListState = MutableStateFlow(VideosByPlayListState()) + private val _videosByPlayListState = MutableStateFlow(VideosByPlayListState()) + val videosByPlayListState = _videosByPlayListState.asStateFlow() private var pageTokenForPlayList: String? = null private val videosByPlayListPagination = getVideosPaginator() private val _leetcodeUpcomingContestsState = MutableStateFlow(LeetcodeUpcomingContestsState()) + val leetcodeUpcomingContestsState = _leetcodeUpcomingContestsState.asStateFlow() - private val userBasicInfo = + private val _userBasicInfo = MutableStateFlow(UserBasicInfo()) + val userBasicInfo = _userBasicInfo.asStateFlow() - private val syncInterval = + private val _syncInterval = MutableStateFlow(IntervalConfigurations.DATA_SYNC_DEFAULT_INTERVAL.minutes) + val syncInterval = _syncInterval.asStateFlow() - private val userContestInfo = + private val _userContestInfo = MutableStateFlow(UserContestInfo()) + val userContestInfo = _userContestInfo.asStateFlow() - private val userProblemSolvedInfo = + private val _userProblemSolvedInfo = MutableStateFlow(UserProblemSolvedInfo()) + val userProblemSolvedInfo = _userProblemSolvedInfo.asStateFlow() - private val isWeeklyGoalSet = MutableStateFlow(false) + private val _isWeeklyGoalSet = MutableStateFlow(false) + val isWeeklyGoalSet = _isWeeklyGoalSet.asStateFlow() private val _dailyProblem = MutableStateFlow(LeetCodeProblem("", "", "")) val dailyProblem = _dailyProblem.asStateFlow() - private val diffStat = MutableStateFlow(DifficultyStatistics()) + private val _difficultyStat = MutableStateFlow(DifficultyStatistics()) + val difficultyStat = _difficultyStat.asStateFlow() val dailyProblemSolved = dailyProblemStatusMonitor.dailyProblemSolved.stateIn( scope = viewModelScope, @@ -101,37 +108,6 @@ class HomeScreenViewModel @Inject constructor( initialValue = false ) - val uiState: StateFlow = - combine( - listOf( - userBasicInfo, - userContestInfo, - userProblemSolvedInfo, - userSubmissionState, - isWeeklyGoalSet, - videosByPlayListState, - _leetcodeUpcomingContestsState, - syncInterval, - diffStat - ) - ) { values -> - UserDetailsUiState( - userBasicInfo = values[0] as UserBasicInfo, - userContestInfo = values[1] as UserContestInfo, - userProblemSolvedInfo = values[2] as UserProblemSolvedInfo, - userSubmissionState = values[3] as UserSubmissionState, - isWeeklyGoalSet = values[4] as Boolean, - videosByPlayListState = values[5] as VideosByPlayListState, - leetcodeUpcomingContestsState = values[6] as LeetcodeUpcomingContestsState, - syncInterval = values[7] as Long, - difficultyStat = values[8] as DifficultyStatistics - ) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = UserDetailsUiState() - ) - init { loadNextAcSubmissions() @@ -140,13 +116,13 @@ class HomeScreenViewModel @Inject constructor( .getUserBasicInfo() .collect { if (it != null) { - userBasicInfo.value = it + _userBasicInfo.value = it } } } viewModelScope.launch { - syncInterval.value = userDatastore.getSyncInterval() + _syncInterval.value = userDatastore.getSyncInterval() } viewModelScope.launch { @@ -154,7 +130,7 @@ class HomeScreenViewModel @Inject constructor( .getUserContestInfo() .collect { if (it != null) { - userContestInfo.value = it + _userContestInfo.value = it } } } @@ -164,7 +140,7 @@ class HomeScreenViewModel @Inject constructor( .getUserProblemSolvedInfo() .collect { if (it != null) { - userProblemSolvedInfo.value = it + _userProblemSolvedInfo.value = it } } } @@ -224,15 +200,15 @@ class HomeScreenViewModel @Inject constructor( private fun getWeeklyGoalStatus() { viewModelScope.launch(Dispatchers.IO) { goalRepository.weeklyGoal.collect { - isWeeklyGoalSet.value = (it != null) + _isWeeklyGoalSet.value = (it != null) } } } private fun getUserSubmissionPaginator() = DefaultPaginator( - initialKey = userSubmissionState.value.page, + initialKey = _userSubmissionState.value.page, onLoadUpdated = { isLoading -> - userSubmissionState.update { + _userSubmissionState.update { it.copy(isLoading = isLoading) } }, @@ -240,15 +216,15 @@ class HomeScreenViewModel @Inject constructor( userDetailsRepository.getUserRecentAcSubmissionsPaginated(nextPage, 5) }, getNextKey = { - userSubmissionState.value.page + 1 + _userSubmissionState.value.page + 1 }, onError = { error -> - userSubmissionState.update { + _userSubmissionState.update { it.copy(error = error?.message) } }, onSuccess = { items, newKey -> - userSubmissionState.update { + _userSubmissionState.update { it.copy( submissions = it.submissions + items, page = newKey, @@ -261,7 +237,7 @@ class HomeScreenViewModel @Inject constructor( private fun getVideosPaginator() = DefaultPaginator( initialKey = pageTokenForPlayList, onLoadUpdated = { isLoading -> - videosByPlayListState.update { + _videosByPlayListState.update { it.copy(isLoading = isLoading) } }, @@ -281,12 +257,12 @@ class HomeScreenViewModel @Inject constructor( pageTokenForPlayList }, onError = { error -> - videosByPlayListState.update { + _videosByPlayListState.update { it.copy(error = error?.message) } }, onSuccess = { items, newKey -> - videosByPlayListState.update { + _videosByPlayListState.update { it.copy( videos = it.videos + items, endReached = items.isEmpty() @@ -316,7 +292,7 @@ class HomeScreenViewModel @Inject constructor( private fun refreshUserSettings() { viewModelScope.launch { - syncInterval.value = userDatastore.getSyncInterval() + _syncInterval.value = userDatastore.getSyncInterval() } } @@ -394,7 +370,7 @@ class HomeScreenViewModel @Inject constructor( private fun getDifficultyStat() = viewModelScope.launch(Dispatchers.IO) { val stat = localProblemRepo.difficultyStat() if (stat.first != 0 && stat.second != 0 && stat.third != 0) { - diffStat.value = DifficultyStatistics( + _difficultyStat.value = DifficultyStatistics( easyProblemCount = stat.first, mediumProblemCount = stat.second, hardProblemCount = stat.third diff --git a/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/model/UserDetailsUiState.kt b/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/model/UserDetailsUiState.kt index 9fe4872..448956d 100644 --- a/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/model/UserDetailsUiState.kt +++ b/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/model/UserDetailsUiState.kt @@ -1,28 +1,9 @@ package com.byteutility.dev.leetcode.plus.ui.screens.home.model -import com.byteutility.dev.leetcode.plus.core.settings.config.IntervalConfigurations -import com.byteutility.dev.leetcode.plus.data.model.UserBasicInfo -import com.byteutility.dev.leetcode.plus.data.model.UserContestInfo -import com.byteutility.dev.leetcode.plus.data.model.UserProblemSolvedInfo import com.byteutility.dev.leetcode.plus.data.model.UserSubmission import com.byteutility.dev.leetcode.plus.network.responseVo.Contest import com.google.api.services.youtube.model.Video -/** - * Created by Shuvo on 11/06/2025. - */ -data class UserDetailsUiState( - val userBasicInfo: UserBasicInfo = UserBasicInfo(), - val userContestInfo: UserContestInfo = UserContestInfo(), - val userProblemSolvedInfo: UserProblemSolvedInfo = UserProblemSolvedInfo(), - val userSubmissionState: UserSubmissionState = UserSubmissionState(), - val isWeeklyGoalSet: Boolean = false, - val syncInterval: Long = IntervalConfigurations.DATA_SYNC_DEFAULT_INTERVAL.minutes, - val videosByPlayListState: VideosByPlayListState = VideosByPlayListState(), - val leetcodeUpcomingContestsState: LeetcodeUpcomingContestsState = LeetcodeUpcomingContestsState(), - val difficultyStat: DifficultyStatistics = DifficultyStatistics() -) - data class LeetcodeUpcomingContestsState( val isLoading: Boolean = false, val contests: List = emptyList(), From c5286ecde7181689ac0b4ba158f4855b8ebf5ad3 Mon Sep 17 00:00:00 2001 From: NiazSagor Date: Mon, 25 May 2026 20:49:41 +0600 Subject: [PATCH 2/4] Refactor weekly goal status tracking in HomeScreenViewModel - Replace manual `MutableStateFlow` and `collect` logic for `isWeeklyGoalSet` with a reactive `StateFlow` derived from `goalRepository.weeklyGoal` using `stateIn`. - Simplify ViewModel initialization and `refreshUiState` by removing the redundant `getWeeklyGoalStatus` function. --- .../ui/screens/home/HomeScreenViewModel.kt | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreenViewModel.kt b/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreenViewModel.kt index fb9f79f..a480044 100644 --- a/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreenViewModel.kt +++ b/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreenViewModel.kt @@ -35,6 +35,7 @@ import com.byteutility.dev.leetcode.plus.ui.screens.home.model.VideosByPlayListS import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -92,8 +93,13 @@ class HomeScreenViewModel @Inject constructor( MutableStateFlow(UserProblemSolvedInfo()) val userProblemSolvedInfo = _userProblemSolvedInfo.asStateFlow() - private val _isWeeklyGoalSet = MutableStateFlow(false) - val isWeeklyGoalSet = _isWeeklyGoalSet.asStateFlow() + val isWeeklyGoalSet: StateFlow = goalRepository.weeklyGoal + .map { it != null } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = false + ) private val _dailyProblem = MutableStateFlow(LeetCodeProblem("", "", "")) @@ -185,8 +191,6 @@ class HomeScreenViewModel @Inject constructor( scheduleBackgroundTasks() - getWeeklyGoalStatus() - getRemoteProblems() getDifficultyStat() @@ -194,15 +198,6 @@ class HomeScreenViewModel @Inject constructor( fun refreshUiState() { refreshUserSettings() - getWeeklyGoalStatus() - } - - private fun getWeeklyGoalStatus() { - viewModelScope.launch(Dispatchers.IO) { - goalRepository.weeklyGoal.collect { - _isWeeklyGoalSet.value = (it != null) - } - } } private fun getUserSubmissionPaginator() = DefaultPaginator( From 49a53911fd1d9dddd35cd5471077fa61872d96d6 Mon Sep 17 00:00:00 2001 From: NiazSagor Date: Mon, 25 May 2026 20:59:40 +0600 Subject: [PATCH 3/4] Refactor HomeScreenViewModel to use stateIn for reactive data streams - Convert `userBasicInfo`, `userContestInfo`, `userProblemSolvedInfo`, and `dailyProblem` from manual `MutableStateFlow` updates to `StateFlow` using the `stateIn` operator. - Implement `SharingStarted.WhileSubscribed(5000)` strategy to improve resource management and handle configuration changes efficiently. - Remove redundant manual flow collection logic and coroutine launches from the `init` block. --- .../ui/screens/home/HomeScreenViewModel.kt | 81 +++++++------------ 1 file changed, 31 insertions(+), 50 deletions(-) diff --git a/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreenViewModel.kt b/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreenViewModel.kt index a480044..1a5d4c4 100644 --- a/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreenViewModel.kt +++ b/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreenViewModel.kt @@ -35,6 +35,9 @@ import com.byteutility.dev.leetcode.plus.ui.screens.home.model.VideosByPlayListS import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -77,21 +80,33 @@ class HomeScreenViewModel @Inject constructor( private val _leetcodeUpcomingContestsState = MutableStateFlow(LeetcodeUpcomingContestsState()) val leetcodeUpcomingContestsState = _leetcodeUpcomingContestsState.asStateFlow() - private val _userBasicInfo = - MutableStateFlow(UserBasicInfo()) - val userBasicInfo = _userBasicInfo.asStateFlow() + val userBasicInfo: StateFlow = flow { + emitAll(userDetailsRepository.getUserBasicInfo().filterNotNull()) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = UserBasicInfo() + ) private val _syncInterval = MutableStateFlow(IntervalConfigurations.DATA_SYNC_DEFAULT_INTERVAL.minutes) val syncInterval = _syncInterval.asStateFlow() - private val _userContestInfo = - MutableStateFlow(UserContestInfo()) - val userContestInfo = _userContestInfo.asStateFlow() + val userContestInfo: StateFlow = flow { + emitAll(userDetailsRepository.getUserContestInfo().filterNotNull()) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = UserContestInfo() + ) - private val _userProblemSolvedInfo = - MutableStateFlow(UserProblemSolvedInfo()) - val userProblemSolvedInfo = _userProblemSolvedInfo.asStateFlow() + val userProblemSolvedInfo: StateFlow = flow { + emitAll(userDetailsRepository.getUserProblemSolvedInfo().filterNotNull()) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = UserProblemSolvedInfo() + ) val isWeeklyGoalSet: StateFlow = goalRepository.weeklyGoal .map { it != null } @@ -101,9 +116,13 @@ class HomeScreenViewModel @Inject constructor( initialValue = false ) - private val _dailyProblem = - MutableStateFlow(LeetCodeProblem("", "", "")) - val dailyProblem = _dailyProblem.asStateFlow() + val dailyProblem: StateFlow = flow { + emitAll(userDetailsRepository.getDailyProblem().filterNotNull()) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = LeetCodeProblem("", "", "") + ) private val _difficultyStat = MutableStateFlow(DifficultyStatistics()) val difficultyStat = _difficultyStat.asStateFlow() @@ -117,40 +136,10 @@ class HomeScreenViewModel @Inject constructor( init { loadNextAcSubmissions() - viewModelScope.launch { - userDetailsRepository - .getUserBasicInfo() - .collect { - if (it != null) { - _userBasicInfo.value = it - } - } - } - viewModelScope.launch { _syncInterval.value = userDatastore.getSyncInterval() } - viewModelScope.launch { - userDetailsRepository - .getUserContestInfo() - .collect { - if (it != null) { - _userContestInfo.value = it - } - } - } - - viewModelScope.launch { - userDetailsRepository - .getUserProblemSolvedInfo() - .collect { - if (it != null) { - _userProblemSolvedInfo.value = it - } - } - } - viewModelScope.launch(Dispatchers.IO) { val formatter = DateTimeFormatter.ofPattern("dd MMMM yyyy") goalRepository.weeklyGoal.collect { goal -> @@ -163,14 +152,6 @@ class HomeScreenViewModel @Inject constructor( } } - viewModelScope.launch(Dispatchers.IO) { - userDetailsRepository.getDailyProblem().collect { - if (it != null) { - _dailyProblem.value = it - } - } - } - viewModelScope.launch(Dispatchers.IO) { _leetcodeUpcomingContestsState.update { it.copy(isLoading = true) From d35ab360b39b57cadf72cf40f75cc5d35048aba0 Mon Sep 17 00:00:00 2001 From: NiazSagor Date: Mon, 25 May 2026 21:07:14 +0600 Subject: [PATCH 4/4] Refactor HomeScreen to hoist state and remove direct ViewModel dependencies - Hoist state collection from `HomeScreenViewModel` to the top-level `HomeScreen` composable using `collectAsStateWithLifecycle`. - Update `HomeLayout`, `HomeTopBar`, and `HomeContent` to accept explicit state parameters instead of a `ViewModel` instance. - Pass individual state properties, including `userBasicInfo`, `userContestInfo`, `userSubmissionState`, and `difficultyStat`, down the composable tree. - Improve component testability and separation of concerns by removing internal `ViewModel` dependencies within sub-composables. --- .../plus/ui/screens/home/HomeScreen.kt | 85 ++++++++++++++----- 1 file changed, 62 insertions(+), 23 deletions(-) diff --git a/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreen.kt index 046929b..d6bc6c7 100644 --- a/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/byteutility/dev/leetcode/plus/ui/screens/home/HomeScreen.kt @@ -107,6 +107,8 @@ import com.byteutility.dev.leetcode.plus.network.responseVo.Contest import com.byteutility.dev.leetcode.plus.ui.common.ProgressIndicator import com.byteutility.dev.leetcode.plus.ui.model.YouTubeVideo import com.byteutility.dev.leetcode.plus.ui.screens.home.model.DifficultyStatistics +import com.byteutility.dev.leetcode.plus.ui.screens.home.model.LeetcodeUpcomingContestsState +import com.byteutility.dev.leetcode.plus.ui.screens.home.model.UserSubmissionState import com.byteutility.dev.leetcode.plus.ui.screens.home.model.VideosByPlayListState import com.byteutility.dev.leetcode.plus.ui.theme.EasyText import com.byteutility.dev.leetcode.plus.ui.theme.HardText @@ -138,12 +140,33 @@ fun HomeScreen( onLogout: () -> Unit = {} ) { val viewModel: HomeScreenViewModel = hiltViewModel() + val isWeeklyGoalSet by viewModel.isWeeklyGoalSet.collectAsStateWithLifecycle() + val userBasicInfo by viewModel.userBasicInfo.collectAsStateWithLifecycle() + val syncInterval by viewModel.syncInterval.collectAsStateWithLifecycle() + val userContestInfo by viewModel.userContestInfo.collectAsStateWithLifecycle() + val userProblemSolvedInfo by viewModel.userProblemSolvedInfo.collectAsStateWithLifecycle() + val userSubmissionState by viewModel.userSubmissionState.collectAsStateWithLifecycle() + val videosByPlayListState by viewModel.videosByPlayListState.collectAsStateWithLifecycle() + val leetcodeUpcomingContestsState by viewModel.leetcodeUpcomingContestsState.collectAsStateWithLifecycle() + val difficultyStat by viewModel.difficultyStat.collectAsStateWithLifecycle() + val dailyProblem by viewModel.dailyProblem.collectAsStateWithLifecycle() + val dailyProblemSolved by viewModel.dailyProblemSolved.collectAsStateWithLifecycle() LifecycleResumeEffect(Unit) { viewModel.refreshUiState() onPauseOrDispose { } } HomeLayout( - viewModel = viewModel, + isWeeklyGoalSet = isWeeklyGoalSet, + userBasicInfo = userBasicInfo, + syncInterval = syncInterval, + userContestInfo = userContestInfo, + userProblemSolvedInfo = userProblemSolvedInfo, + userSubmissionState = userSubmissionState, + videosByPlayListState = videosByPlayListState, + leetcodeUpcomingContestsState = leetcodeUpcomingContestsState, + difficultyStat = difficultyStat, + dailyProblem = dailyProblem, + dailyProblemSolved = dailyProblemSolved, onSetGoal = onSetGoal, onGoalStatus = onGoalStatus, onTroubleShoot = onTroubleShoot, @@ -173,7 +196,17 @@ fun HomeScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeLayout( - viewModel: HomeScreenViewModel, + isWeeklyGoalSet: Boolean, + userBasicInfo: UserBasicInfo, + syncInterval: Long, + userContestInfo: UserContestInfo, + userProblemSolvedInfo: UserProblemSolvedInfo, + userSubmissionState: UserSubmissionState, + videosByPlayListState: VideosByPlayListState, + leetcodeUpcomingContestsState: LeetcodeUpcomingContestsState, + difficultyStat: DifficultyStatistics, + dailyProblem: LeetCodeProblem, + dailyProblemSolved: Boolean, onSetGoal: () -> Unit, onGoalStatus: () -> Unit, onTroubleShoot: () -> Unit, @@ -206,7 +239,8 @@ fun HomeLayout( Scaffold( topBar = { HomeTopBar( - viewModel = viewModel, + isWeeklyGoalSet = isWeeklyGoalSet, + avatarUrl = userBasicInfo.avatar, clickCount = clickCount, lastClickTime = lastClickTime, onClickCountChange = { clickCount = it }, @@ -230,7 +264,16 @@ fun HomeLayout( .weight(1f) ) { HomeContent( - viewModel = viewModel, + userBasicInfo = userBasicInfo, + syncInterval = syncInterval, + userContestInfo = userContestInfo, + userProblemSolvedInfo = userProblemSolvedInfo, + userSubmissionState = userSubmissionState, + videosByPlayListState = videosByPlayListState, + leetcodeUpcomingContestsState = leetcodeUpcomingContestsState, + difficultyStat = difficultyStat, + dailyProblem = dailyProblem, + dailyProblemSolved = dailyProblemSolved, onNavigateToProblemDetails = onNavigateToProblemDetails, onLoadMoreSubmission = onLoadMoreSubmission, onLoadMoreVideos = onLoadMoreVideos, @@ -295,7 +338,8 @@ fun HomeLayout( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun HomeTopBar( - viewModel: HomeScreenViewModel, + isWeeklyGoalSet: Boolean, + avatarUrl: String, clickCount: Int, lastClickTime: Long, onClickCountChange: (Int) -> Unit, @@ -306,9 +350,6 @@ private fun HomeTopBar( onTroubleShoot: () -> Unit, onLogoutClick: () -> Unit ) { - val isWeeklyGoalSet by viewModel.isWeeklyGoalSet.collectAsStateWithLifecycle() - val userBasicInfo by viewModel.userBasicInfo.collectAsStateWithLifecycle() - TopAppBar( title = { Text( @@ -337,7 +378,7 @@ private fun HomeTopBar( actions = { MainTopActions( isWeeklyGoalSet = isWeeklyGoalSet, - avatarUrl = userBasicInfo.avatar, + avatarUrl = avatarUrl, onSetGoal = onSetGoal, onGoalStatus = onGoalStatus, onLogoutClick = onLogoutClick, @@ -382,7 +423,16 @@ fun MainTopActions( @Composable private fun HomeContent( - viewModel: HomeScreenViewModel, + userBasicInfo: UserBasicInfo, + syncInterval: Long, + userContestInfo: UserContestInfo, + userProblemSolvedInfo: UserProblemSolvedInfo, + userSubmissionState: UserSubmissionState, + videosByPlayListState: VideosByPlayListState, + leetcodeUpcomingContestsState: LeetcodeUpcomingContestsState, + difficultyStat: DifficultyStatistics, + dailyProblem: LeetCodeProblem, + dailyProblemSolved: Boolean, onNavigateToProblemDetails: (String) -> Unit, onLoadMoreSubmission: () -> Unit, onLoadMoreVideos: () -> Unit, @@ -392,17 +442,6 @@ private fun HomeContent( onNavigateToContestDetail: (Contest) -> Unit = {}, modifier: Modifier = Modifier, ) { - val userBasicInfo by viewModel.userBasicInfo.collectAsStateWithLifecycle() - val syncInterval by viewModel.syncInterval.collectAsStateWithLifecycle() - val userContestInfo by viewModel.userContestInfo.collectAsStateWithLifecycle() - val userProblemSolvedInfo by viewModel.userProblemSolvedInfo.collectAsStateWithLifecycle() - val userSubmissionState by viewModel.userSubmissionState.collectAsStateWithLifecycle() - val videosByPlayListState by viewModel.videosByPlayListState.collectAsStateWithLifecycle() - val leetcodeUpcomingContestsState by viewModel.leetcodeUpcomingContestsState.collectAsStateWithLifecycle() - val difficultyStat by viewModel.difficultyStat.collectAsStateWithLifecycle() - val dailyProblem by viewModel.dailyProblem.collectAsStateWithLifecycle() - val dailyProblemSolved by viewModel.dailyProblemSolved.collectAsStateWithLifecycle() - UserProfileContent( userBasicInfo = userBasicInfo, syncInterval = syncInterval, @@ -431,9 +470,9 @@ fun UserProfileContent( syncInterval: Long, userContestInfo: UserContestInfo, userProblemSolvedInfo: UserProblemSolvedInfo, - userSubmissionState: com.byteutility.dev.leetcode.plus.ui.screens.home.model.UserSubmissionState, + userSubmissionState: UserSubmissionState, videosByPlayListState: VideosByPlayListState, - leetcodeUpcomingContestsState: com.byteutility.dev.leetcode.plus.ui.screens.home.model.LeetcodeUpcomingContestsState, + leetcodeUpcomingContestsState: LeetcodeUpcomingContestsState, difficultyStat: DifficultyStatistics, dailyProblem: LeetCodeProblem, dailyProblemSolved: Boolean,