diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastsRepository.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastsRepository.kt index cb9b308405..a102caf961 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastsRepository.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastsRepository.kt @@ -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 } @@ -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() } } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt index ce2d128a2c..a7658d71dd 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt @@ -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 @@ -134,6 +134,7 @@ import kotlinx.coroutines.launch data class HomeState( val windowSizeClass: WindowSizeClass, + val isLoading: Boolean, val featuredPodcasts: PersistentList, val selectedHomeCategory: HomeCategory, val homeCategories: List, @@ -180,10 +181,12 @@ fun calculateScaffoldDirective( maxHorizontalPartitions = 1 verticalSpacerSize = 0.dp } + WindowWidthSizeClass.MEDIUM -> { maxHorizontalPartitions = 1 verticalSpacerSize = 0.dp } + else -> { maxHorizontalPartitions = 2 verticalSpacerSize = 24.dp @@ -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) } } } @@ -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() @@ -302,6 +295,7 @@ private fun HomeScreenReady( val homeState = HomeState( windowSizeClass = windowSizeClass, + isLoading = uiState.isLoading, featuredPodcasts = uiState.featuredPodcasts, homeCategories = uiState.homeCategories, selectedHomeCategory = uiState.selectedHomeCategory, @@ -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) @@ -811,6 +814,7 @@ private fun PreviewHome() { JetcasterTheme { val homeState = HomeState( windowSizeClass = CompactWindowSizeClass, + isLoading = true, featuredPodcasts = PreviewPodcasts.toPersistentList(), homeCategories = HomeCategory.entries, selectedHomeCategory = HomeCategory.Discover, diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt index 3d5450464d..7976a8b6d8 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt @@ -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 @@ -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, @@ -61,14 +60,19 @@ class HomeViewModel @Inject constructor( ) : ViewModel() { // Holds our currently selected podcast in the library private val selectedLibraryPodcast = MutableStateFlow(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(null) + // Holds our view state which the UI collects via [state] - private val _state = MutableStateFlow(HomeScreenUiState.Loading) + private val _state = MutableStateFlow(HomeScreenUiState()) + // Holds the view state if the UI is refreshing for new data private val refreshing = MutableStateFlow(false) @@ -107,11 +111,6 @@ 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 @@ -119,7 +118,8 @@ class HomeViewModel @Inject constructor( selectedHomeCategory.value = if (podcasts.isEmpty()) HomeCategory.Discover else homeCategory - HomeScreenUiState.Ready( + HomeScreenUiState( + isLoading = refreshing, homeCategories = homeCategories, selectedHomeCategory = homeCategory, featuredPodcasts = podcasts.map { it.asExternalModel() }.toPersistentList(), @@ -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 } @@ -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 = persistentListOf(), - val selectedHomeCategory: HomeCategory = HomeCategory.Discover, - val homeCategories: List = 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 = persistentListOf(), + val selectedHomeCategory: HomeCategory = HomeCategory.Discover, + val homeCategories: List = emptyList(), + val filterableCategoriesModel: FilterableCategoriesModel = FilterableCategoriesModel(), + val podcastCategoryFilterResult: PodcastCategoryFilterResult = PodcastCategoryFilterResult(), + val library: LibraryInfo = LibraryInfo(), +)