Skip to content

Commit 9f2103f

Browse files
authored
[Jetcaster] Refactor home state and fix loading progress (#1499)
[Merge after #1498] - This PR merges the home UI state into one data class to be able to show loading state and content state at the same time. - It also fixes the progress to wait for the loading to finish. - Instead of circular progress, I switched to linear progress at the top of the content, which can indicate the work while showing content **After** https://github.com/user-attachments/assets/ec3e9220-addc-4b96-9b6a-477294b6cf85 **Before** https://github.com/user-attachments/assets/c795afc3-01f6-407b-b0b6-505da33652ec
2 parents 6074a82 + 185d460 commit 9f2103f

File tree

3 files changed

+60
-56
lines changed

3 files changed

+60
-56
lines changed

Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastsRepository.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ class PodcastsRepository @Inject constructor(
4949
if (refreshingJob?.isActive == true) {
5050
refreshingJob?.join()
5151
} else if (force || podcastStore.isEmpty()) {
52-
53-
refreshingJob = scope.launch {
52+
val job = scope.launch {
5453
// Now fetch the podcasts, and add each to each store
5554
podcastsFetcher(SampleFeeds)
5655
.filter { it is PodcastRssResponse.Success }
@@ -72,6 +71,9 @@ class PodcastsRepository @Inject constructor(
7271
}
7372
}
7473
}
74+
refreshingJob = job
75+
// We need to wait here for the job to finish, otherwise the coroutine completes ~immediatelly
76+
job.join()
7577
}
7678
}
7779
}

Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ import androidx.compose.material.icons.Icons
5050
import androidx.compose.material.icons.filled.AccountCircle
5151
import androidx.compose.material.icons.filled.Search
5252
import androidx.compose.material3.Button
53-
import androidx.compose.material3.CircularProgressIndicator
5453
import androidx.compose.material3.ExperimentalMaterial3Api
5554
import androidx.compose.material3.HorizontalDivider
5655
import androidx.compose.material3.Icon
56+
import androidx.compose.material3.LinearProgressIndicator
5757
import androidx.compose.material3.MaterialTheme
5858
import androidx.compose.material3.Scaffold
5959
import androidx.compose.material3.SearchBar
@@ -134,6 +134,7 @@ import kotlinx.coroutines.launch
134134

135135
data class HomeState(
136136
val windowSizeClass: WindowSizeClass,
137+
val isLoading: Boolean,
137138
val featuredPodcasts: PersistentList<PodcastInfo>,
138139
val selectedHomeCategory: HomeCategory,
139140
val homeCategories: List<HomeCategory>,
@@ -180,10 +181,12 @@ fun calculateScaffoldDirective(
180181
maxHorizontalPartitions = 1
181182
verticalSpacerSize = 0.dp
182183
}
184+
183185
WindowWidthSizeClass.MEDIUM -> {
184186
maxHorizontalPartitions = 1
185187
verticalSpacerSize = 0.dp
186188
}
189+
187190
else -> {
188191
maxHorizontalPartitions = 2
189192
verticalSpacerSize = 24.dp
@@ -233,27 +236,17 @@ fun MainScreen(
233236
viewModel: HomeViewModel = hiltViewModel()
234237
) {
235238
val homeScreenUiState by viewModel.state.collectAsStateWithLifecycle()
236-
when (val uiState = homeScreenUiState) {
237-
is HomeScreenUiState.Loading -> HomeScreenLoading()
238-
is HomeScreenUiState.Error -> HomeScreenError(onRetry = viewModel::refresh)
239-
is HomeScreenUiState.Ready -> {
240-
HomeScreenReady(
241-
uiState = uiState,
242-
windowSizeClass = windowSizeClass,
243-
navigateToPlayer = navigateToPlayer,
244-
viewModel = viewModel,
245-
)
246-
}
247-
}
248-
}
239+
val uiState = homeScreenUiState
240+
Box {
241+
HomeScreenReady(
242+
uiState = uiState,
243+
windowSizeClass = windowSizeClass,
244+
navigateToPlayer = navigateToPlayer,
245+
viewModel = viewModel,
246+
)
249247

250-
@Composable
251-
private fun HomeScreenLoading(modifier: Modifier = Modifier) {
252-
Surface(modifier.fillMaxSize()) {
253-
Box {
254-
CircularProgressIndicator(
255-
modifier = Modifier.align(Alignment.Center)
256-
)
248+
if (uiState.errorMessage != null) {
249+
HomeScreenError(onRetry = viewModel::refresh)
257250
}
258251
}
259252
}
@@ -288,7 +281,7 @@ fun HomeScreenErrorPreview() {
288281
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
289282
@Composable
290283
private fun HomeScreenReady(
291-
uiState: HomeScreenUiState.Ready,
284+
uiState: HomeScreenUiState,
292285
windowSizeClass: WindowSizeClass,
293286
navigateToPlayer: (EpisodeInfo) -> Unit,
294287
viewModel: HomeViewModel = hiltViewModel()
@@ -302,6 +295,7 @@ private fun HomeScreenReady(
302295

303296
val homeState = HomeState(
304297
windowSizeClass = windowSizeClass,
298+
isLoading = uiState.isLoading,
305299
featuredPodcasts = uiState.featuredPodcasts,
306300
homeCategories = uiState.homeCategories,
307301
selectedHomeCategory = uiState.selectedHomeCategory,
@@ -443,10 +437,19 @@ private fun HomeScreen(
443437
) {
444438
Scaffold(
445439
topBar = {
446-
HomeAppBar(
447-
isExpanded = homeState.windowSizeClass.isCompact,
448-
modifier = Modifier.fillMaxWidth(),
449-
)
440+
Column {
441+
HomeAppBar(
442+
isExpanded = homeState.windowSizeClass.isCompact,
443+
modifier = Modifier.fillMaxWidth(),
444+
)
445+
if (homeState.isLoading) {
446+
LinearProgressIndicator(
447+
Modifier
448+
.fillMaxWidth()
449+
.padding(horizontal = 16.dp)
450+
)
451+
}
452+
}
450453
},
451454
snackbarHost = {
452455
SnackbarHost(hostState = snackbarHostState)
@@ -811,6 +814,7 @@ private fun PreviewHome() {
811814
JetcasterTheme {
812815
val homeState = HomeState(
813816
windowSizeClass = CompactWindowSizeClass,
817+
isLoading = true,
814818
featuredPodcasts = PreviewPodcasts.toPersistentList(),
815819
homeCategories = HomeCategory.entries,
816820
selectedHomeCategory = HomeCategory.Discover,

Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package com.example.jetcaster.ui.home
1818

19-
import android.util.Log
19+
import androidx.compose.runtime.Immutable
2020
import androidx.lifecycle.ViewModel
2121
import androidx.lifecycle.viewModelScope
2222
import com.example.jetcaster.core.data.database.model.EpisodeToPodcast
@@ -49,7 +49,6 @@ import kotlinx.coroutines.flow.shareIn
4949
import kotlinx.coroutines.launch
5050

5151
@OptIn(ExperimentalCoroutinesApi::class)
52-
5352
@HiltViewModel
5453
class HomeViewModel @Inject constructor(
5554
private val podcastsRepository: PodcastsRepository,
@@ -61,14 +60,19 @@ class HomeViewModel @Inject constructor(
6160
) : ViewModel() {
6261
// Holds our currently selected podcast in the library
6362
private val selectedLibraryPodcast = MutableStateFlow<PodcastInfo?>(null)
63+
6464
// Holds our currently selected home category
6565
private val selectedHomeCategory = MutableStateFlow(HomeCategory.Discover)
66+
6667
// Holds the currently available home categories
6768
private val homeCategories = MutableStateFlow(HomeCategory.entries)
69+
6870
// Holds our currently selected category
6971
private val _selectedCategory = MutableStateFlow<CategoryInfo?>(null)
72+
7073
// Holds our view state which the UI collects via [state]
71-
private val _state = MutableStateFlow<HomeScreenUiState>(HomeScreenUiState.Loading)
74+
private val _state = MutableStateFlow(HomeScreenUiState())
75+
7276
// Holds the view state if the UI is refreshing for new data
7377
private val refreshing = MutableStateFlow(false)
7478

@@ -107,19 +111,15 @@ class HomeViewModel @Inject constructor(
107111
podcastCategoryFilterResult,
108112
libraryEpisodes ->
109113

110-
if (refreshing) {
111-
Log.d("Jetcaster", "refreshing: $refreshing, podcasts $podcasts")
112-
return@combine HomeScreenUiState.Loading
113-
}
114-
115114
_selectedCategory.value = filterableCategories.selectedCategory
116115

117116
// Override selected home category to show 'DISCOVER' if there are no
118117
// featured podcasts
119118
selectedHomeCategory.value =
120119
if (podcasts.isEmpty()) HomeCategory.Discover else homeCategory
121120

122-
HomeScreenUiState.Ready(
121+
HomeScreenUiState(
122+
isLoading = refreshing,
123123
homeCategories = homeCategories,
124124
selectedHomeCategory = homeCategory,
125125
featuredPodcasts = podcasts.map { it.asExternalModel() }.toPersistentList(),
@@ -128,7 +128,12 @@ class HomeViewModel @Inject constructor(
128128
library = libraryEpisodes.asLibrary()
129129
)
130130
}.catch { throwable ->
131-
_state.value = HomeScreenUiState.Error(throwable.message)
131+
emit(
132+
HomeScreenUiState(
133+
isLoading = false,
134+
errorMessage = throwable.message
135+
)
136+
)
132137
}.collect {
133138
_state.value = it
134139
}
@@ -187,21 +192,14 @@ enum class HomeCategory {
187192
Library, Discover
188193
}
189194

190-
sealed interface HomeScreenUiState {
191-
data object Loading : HomeScreenUiState
192-
193-
data class Error(
194-
val errorMessage: String? = null
195-
) : HomeScreenUiState
196-
197-
data class Ready(
198-
val featuredPodcasts: PersistentList<PodcastInfo> = persistentListOf(),
199-
val selectedHomeCategory: HomeCategory = HomeCategory.Discover,
200-
val homeCategories: List<HomeCategory> = emptyList(),
201-
val filterableCategoriesModel: FilterableCategoriesModel =
202-
FilterableCategoriesModel(),
203-
val podcastCategoryFilterResult: PodcastCategoryFilterResult =
204-
PodcastCategoryFilterResult(),
205-
val library: LibraryInfo = LibraryInfo(),
206-
) : HomeScreenUiState
207-
}
195+
@Immutable
196+
data class HomeScreenUiState(
197+
val isLoading: Boolean = true,
198+
val errorMessage: String? = null,
199+
val featuredPodcasts: PersistentList<PodcastInfo> = persistentListOf(),
200+
val selectedHomeCategory: HomeCategory = HomeCategory.Discover,
201+
val homeCategories: List<HomeCategory> = emptyList(),
202+
val filterableCategoriesModel: FilterableCategoriesModel = FilterableCategoriesModel(),
203+
val podcastCategoryFilterResult: PodcastCategoryFilterResult = PodcastCategoryFilterResult(),
204+
val library: LibraryInfo = LibraryInfo(),
205+
)

0 commit comments

Comments
 (0)