Skip to content

Commit

Permalink
[Jetcaster] Refactor home state and fix loading progress (#1499)
Browse files Browse the repository at this point in the history
[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
  • Loading branch information
mlykotom authored Nov 18, 2024
2 parents 6074a82 + 185d460 commit 9f2103f
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ class PodcastsRepository @Inject constructor(
if (refreshingJob?.isActive == true) {
refreshingJob?.join()
} else if (force || podcastStore.isEmpty()) {

refreshingJob = scope.launch {
val job = scope.launch {
// Now fetch the podcasts, and add each to each store
podcastsFetcher(SampleFeeds)
.filter { it is PodcastRssResponse.Success }
Expand All @@ -72,6 +71,9 @@ class PodcastsRepository @Inject constructor(
}
}
}
refreshingJob = job
// We need to wait here for the job to finish, otherwise the coroutine completes ~immediatelly
job.join()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
Expand Down Expand Up @@ -134,6 +134,7 @@ import kotlinx.coroutines.launch

data class HomeState(
val windowSizeClass: WindowSizeClass,
val isLoading: Boolean,
val featuredPodcasts: PersistentList<PodcastInfo>,
val selectedHomeCategory: HomeCategory,
val homeCategories: List<HomeCategory>,
Expand Down Expand Up @@ -180,10 +181,12 @@ fun calculateScaffoldDirective(
maxHorizontalPartitions = 1
verticalSpacerSize = 0.dp
}

WindowWidthSizeClass.MEDIUM -> {
maxHorizontalPartitions = 1
verticalSpacerSize = 0.dp
}

else -> {
maxHorizontalPartitions = 2
verticalSpacerSize = 24.dp
Expand Down Expand Up @@ -233,27 +236,17 @@ fun MainScreen(
viewModel: HomeViewModel = hiltViewModel()
) {
val homeScreenUiState by viewModel.state.collectAsStateWithLifecycle()
when (val uiState = homeScreenUiState) {
is HomeScreenUiState.Loading -> HomeScreenLoading()
is HomeScreenUiState.Error -> HomeScreenError(onRetry = viewModel::refresh)
is HomeScreenUiState.Ready -> {
HomeScreenReady(
uiState = uiState,
windowSizeClass = windowSizeClass,
navigateToPlayer = navigateToPlayer,
viewModel = viewModel,
)
}
}
}
val uiState = homeScreenUiState
Box {
HomeScreenReady(
uiState = uiState,
windowSizeClass = windowSizeClass,
navigateToPlayer = navigateToPlayer,
viewModel = viewModel,
)

@Composable
private fun HomeScreenLoading(modifier: Modifier = Modifier) {
Surface(modifier.fillMaxSize()) {
Box {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
if (uiState.errorMessage != null) {
HomeScreenError(onRetry = viewModel::refresh)
}
}
}
Expand Down Expand Up @@ -288,7 +281,7 @@ fun HomeScreenErrorPreview() {
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
private fun HomeScreenReady(
uiState: HomeScreenUiState.Ready,
uiState: HomeScreenUiState,
windowSizeClass: WindowSizeClass,
navigateToPlayer: (EpisodeInfo) -> Unit,
viewModel: HomeViewModel = hiltViewModel()
Expand All @@ -302,6 +295,7 @@ private fun HomeScreenReady(

val homeState = HomeState(
windowSizeClass = windowSizeClass,
isLoading = uiState.isLoading,
featuredPodcasts = uiState.featuredPodcasts,
homeCategories = uiState.homeCategories,
selectedHomeCategory = uiState.selectedHomeCategory,
Expand Down Expand Up @@ -443,10 +437,19 @@ private fun HomeScreen(
) {
Scaffold(
topBar = {
HomeAppBar(
isExpanded = homeState.windowSizeClass.isCompact,
modifier = Modifier.fillMaxWidth(),
)
Column {
HomeAppBar(
isExpanded = homeState.windowSizeClass.isCompact,
modifier = Modifier.fillMaxWidth(),
)
if (homeState.isLoading) {
LinearProgressIndicator(
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
}
}
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
Expand Down Expand Up @@ -811,6 +814,7 @@ private fun PreviewHome() {
JetcasterTheme {
val homeState = HomeState(
windowSizeClass = CompactWindowSizeClass,
isLoading = true,
featuredPodcasts = PreviewPodcasts.toPersistentList(),
homeCategories = HomeCategory.entries,
selectedHomeCategory = HomeCategory.Discover,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package com.example.jetcaster.ui.home

import android.util.Log
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.jetcaster.core.data.database.model.EpisodeToPodcast
Expand Down Expand Up @@ -49,7 +49,6 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch

@OptIn(ExperimentalCoroutinesApi::class)

@HiltViewModel
class HomeViewModel @Inject constructor(
private val podcastsRepository: PodcastsRepository,
Expand All @@ -61,14 +60,19 @@ class HomeViewModel @Inject constructor(
) : ViewModel() {
// Holds our currently selected podcast in the library
private val selectedLibraryPodcast = MutableStateFlow<PodcastInfo?>(null)

// Holds our currently selected home category
private val selectedHomeCategory = MutableStateFlow(HomeCategory.Discover)

// Holds the currently available home categories
private val homeCategories = MutableStateFlow(HomeCategory.entries)

// Holds our currently selected category
private val _selectedCategory = MutableStateFlow<CategoryInfo?>(null)

// Holds our view state which the UI collects via [state]
private val _state = MutableStateFlow<HomeScreenUiState>(HomeScreenUiState.Loading)
private val _state = MutableStateFlow(HomeScreenUiState())

// Holds the view state if the UI is refreshing for new data
private val refreshing = MutableStateFlow(false)

Expand Down Expand Up @@ -107,19 +111,15 @@ class HomeViewModel @Inject constructor(
podcastCategoryFilterResult,
libraryEpisodes ->

if (refreshing) {
Log.d("Jetcaster", "refreshing: $refreshing, podcasts $podcasts")
return@combine HomeScreenUiState.Loading
}

_selectedCategory.value = filterableCategories.selectedCategory

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

HomeScreenUiState.Ready(
HomeScreenUiState(
isLoading = refreshing,
homeCategories = homeCategories,
selectedHomeCategory = homeCategory,
featuredPodcasts = podcasts.map { it.asExternalModel() }.toPersistentList(),
Expand All @@ -128,7 +128,12 @@ class HomeViewModel @Inject constructor(
library = libraryEpisodes.asLibrary()
)
}.catch { throwable ->
_state.value = HomeScreenUiState.Error(throwable.message)
emit(
HomeScreenUiState(
isLoading = false,
errorMessage = throwable.message
)
)
}.collect {
_state.value = it
}
Expand Down Expand Up @@ -187,21 +192,14 @@ enum class HomeCategory {
Library, Discover
}

sealed interface HomeScreenUiState {
data object Loading : HomeScreenUiState

data class Error(
val errorMessage: String? = null
) : HomeScreenUiState

data class Ready(
val featuredPodcasts: PersistentList<PodcastInfo> = persistentListOf(),
val selectedHomeCategory: HomeCategory = HomeCategory.Discover,
val homeCategories: List<HomeCategory> = emptyList(),
val filterableCategoriesModel: FilterableCategoriesModel =
FilterableCategoriesModel(),
val podcastCategoryFilterResult: PodcastCategoryFilterResult =
PodcastCategoryFilterResult(),
val library: LibraryInfo = LibraryInfo(),
) : HomeScreenUiState
}
@Immutable
data class HomeScreenUiState(
val isLoading: Boolean = true,
val errorMessage: String? = null,
val featuredPodcasts: PersistentList<PodcastInfo> = persistentListOf(),
val selectedHomeCategory: HomeCategory = HomeCategory.Discover,
val homeCategories: List<HomeCategory> = emptyList(),
val filterableCategoriesModel: FilterableCategoriesModel = FilterableCategoriesModel(),
val podcastCategoryFilterResult: PodcastCategoryFilterResult = PodcastCategoryFilterResult(),
val library: LibraryInfo = LibraryInfo(),
)

0 comments on commit 9f2103f

Please sign in to comment.