Skip to content

Commit

Permalink
[Jetcaster] Adding SupportingPaneScaffold to Home (#1303)
Browse files Browse the repository at this point in the history
Adding `SupportingPaneScaffold` to the home screen of Jetcaster.

Remaining TODOs:
- [ ] Make expanded-width default to having the supporting pane be
hidden
- [ ] Hide top app bar in supporting pane when 2 panes are shown



[Screen_recording_20240329_162553.webm](https://github.com/android/compose-samples/assets/463186/a5b185d3-de84-4962-a01f-d4976021dcfa)
  • Loading branch information
arriolac committed Apr 1, 2024
2 parents 6058f86 + c89178c commit 8223314
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 148 deletions.
4 changes: 4 additions & 0 deletions Jetcaster/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ dependencies {
implementation(libs.androidx.constraintlayout.compose)

implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.compose.material3.adaptive.layout)
implementation(libs.androidx.compose.material3.adaptive.navigation)
implementation(libs.androidx.compose.material3.window)
implementation(libs.androidx.compose.material.iconsExtended)
implementation(libs.androidx.compose.ui.tooling.preview)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ fun JetcasterApp(
) {
composable(Screen.Home.route) { backStackEntry ->
Home(
navigateToPodcastDetails = { podcast ->
appState.navigateToPodcastDetails(podcast.uri, backStackEntry)
},
navigateToPlayer = { episode ->
appState.navigateToPlayer(episode.uri, backStackEntry)
}
Expand Down
76 changes: 58 additions & 18 deletions Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

package com.example.jetcaster.ui.home

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
Expand Down Expand Up @@ -63,6 +64,10 @@ import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffold
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
Expand Down Expand Up @@ -92,6 +97,8 @@ import com.example.jetcaster.core.data.model.PodcastCategoryFilterResult
import com.example.jetcaster.core.data.model.PodcastInfo
import com.example.jetcaster.ui.home.discover.discoverItems
import com.example.jetcaster.ui.home.library.libraryItems
import com.example.jetcaster.ui.podcast.PodcastDetailsScreen
import com.example.jetcaster.ui.podcast.PodcastDetailsViewModel
import com.example.jetcaster.ui.theme.JetcasterTheme
import com.example.jetcaster.util.ToggleFollowPodcastIconButton
import com.example.jetcaster.util.quantityStringResource
Expand All @@ -103,30 +110,63 @@ import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Home(
navigateToPodcastDetails: (PodcastInfo) -> Unit,
navigateToPlayer: (EpisodeInfo) -> Unit,
viewModel: HomeViewModel = viewModel()
) {
val viewState by viewModel.state.collectAsStateWithLifecycle()
Surface(Modifier.fillMaxSize()) {
Home(
featuredPodcasts = viewState.featuredPodcasts,
isRefreshing = viewState.refreshing,
homeCategories = viewState.homeCategories,
selectedHomeCategory = viewState.selectedHomeCategory,
filterableCategoriesModel = viewState.filterableCategoriesModel,
podcastCategoryFilterResult = viewState.podcastCategoryFilterResult,
library = viewState.library,
onHomeCategorySelected = viewModel::onHomeCategorySelected,
onCategorySelected = viewModel::onCategorySelected,
onPodcastUnfollowed = viewModel::onPodcastUnfollowed,
navigateToPodcastDetails = navigateToPodcastDetails,
navigateToPlayer = navigateToPlayer,
onTogglePodcastFollowed = viewModel::onTogglePodcastFollowed,
onLibraryPodcastSelected = viewModel::onLibraryPodcastSelected,
onQueueEpisode = viewModel::onQueueEpisode,
val navigator = rememberSupportingPaneScaffoldNavigator<String>(
isDestinationHistoryAware = false
)
BackHandler(enabled = navigator.canNavigateBack()) {
navigator.navigateBack()
}
Surface {
SupportingPaneScaffold(
value = navigator.scaffoldValue,
directive = navigator.scaffoldDirective,
supportingPane = {
val podcastUri = navigator.currentDestination?.content
?: viewState.featuredPodcasts.firstOrNull()?.uri
if (!podcastUri.isNullOrEmpty()) {
val podcastDetailsViewModel = PodcastDetailsViewModel(
podcastUri = podcastUri
)
PodcastDetailsScreen(
viewModel = podcastDetailsViewModel,
navigateToPlayer = navigateToPlayer,
navigateBack = {
if (navigator.canNavigateBack()) {
navigator.navigateBack()
}
}
)
}
},
mainPane = {
Home(
featuredPodcasts = viewState.featuredPodcasts,
isRefreshing = viewState.refreshing,
homeCategories = viewState.homeCategories,
selectedHomeCategory = viewState.selectedHomeCategory,
filterableCategoriesModel = viewState.filterableCategoriesModel,
podcastCategoryFilterResult = viewState.podcastCategoryFilterResult,
library = viewState.library,
onHomeCategorySelected = viewModel::onHomeCategorySelected,
onCategorySelected = viewModel::onCategorySelected,
onPodcastUnfollowed = viewModel::onPodcastUnfollowed,
navigateToPodcastDetails = {
navigator.navigateTo(SupportingPaneScaffoldRole.Supporting, it.uri)
},
navigateToPlayer = navigateToPlayer,
onTogglePodcastFollowed = viewModel::onTogglePodcastFollowed,
onLibraryPodcastSelected = viewModel::onLibraryPodcastSelected,
onQueueEpisode = viewModel::onQueueEpisode,
modifier = Modifier.fillMaxSize()
)
},
modifier = Modifier.fillMaxSize()
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ fun LazyListScope.libraryItems(
onClick = navigateToPlayer,
onQueueEpisode = onQueueEpisode,
modifier = Modifier.fillParentMaxWidth(),
showDivider = index != 0
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import com.example.jetcaster.designsystem.theme.Keyline1
import com.example.jetcaster.ui.home.PreviewEpisodes
import com.example.jetcaster.ui.home.PreviewPodcasts
import com.example.jetcaster.ui.shared.EpisodeListItem
import com.example.jetcaster.ui.shared.Loading
import kotlinx.coroutines.launch

@Composable
Expand All @@ -79,15 +80,31 @@ fun PodcastDetailsScreen(
modifier: Modifier = Modifier
) {
val state by viewModel.state.collectAsStateWithLifecycle()
PodcastDetailsScreen(
podcast = state.podcast,
episodes = state.episodes,
toggleSubscribe = viewModel::toggleSusbcribe,
onQueueEpisode = viewModel::onQueueEpisode,
navigateToPlayer = navigateToPlayer,
navigateBack = navigateBack,
modifier = modifier,
)
when (val s = state) {
is PodcastUiState.Loading -> {
PodcastDetailsLoadingScreen(
modifier = Modifier.fillMaxSize()
)
}
is PodcastUiState.Ready -> {
PodcastDetailsScreen(
podcast = s.podcast,
episodes = s.episodes,
toggleSubscribe = viewModel::toggleSusbcribe,
onQueueEpisode = viewModel::onQueueEpisode,
navigateToPlayer = navigateToPlayer,
navigateBack = navigateBack,
modifier = modifier,
)
}
}
}

@Composable
private fun PodcastDetailsLoadingScreen(
modifier: Modifier = Modifier
) {
Loading(modifier = modifier)
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

data class PodcastUiState(
val podcast: PodcastInfo = PodcastInfo(),
val episodes: List<EpisodeInfo> = emptyList()
)
sealed interface PodcastUiState {
data object Loading : PodcastUiState
data class Ready(
val podcast: PodcastInfo,
val episodes: List<EpisodeInfo>,
) : PodcastUiState
}

/**
* ViewModel that handles the business logic and screen state of the Podcast details screen.
Expand All @@ -50,26 +53,35 @@ class PodcastDetailsViewModel(
private val episodeStore: EpisodeStore = Graph.episodeStore,
private val episodePlayer: EpisodePlayer = Graph.episodePlayer,
private val podcastStore: PodcastStore = Graph.podcastStore,
savedStateHandle: SavedStateHandle
private val podcastUri: String
) : ViewModel() {

private val podcastUri: String =
Uri.decode(savedStateHandle.get<String>(Screen.ARG_PODCAST_URI)!!)
constructor(
episodeStore: EpisodeStore = Graph.episodeStore,
episodePlayer: EpisodePlayer = Graph.episodePlayer,
podcastStore: PodcastStore = Graph.podcastStore,
savedStateHandle: SavedStateHandle
) : this(
episodeStore = episodeStore,
episodePlayer = episodePlayer,
podcastStore = podcastStore,
podcastUri = Uri.decode(savedStateHandle.get<String>(Screen.ARG_PODCAST_URI)!!)
)

val state: StateFlow<PodcastUiState> =
combine(
podcastStore.podcastWithExtraInfo(podcastUri),
episodeStore.episodesInPodcast(podcastUri)
) { podcast, episodeToPodcasts ->
val episodes = episodeToPodcasts.map { it.episode.asExternalModel() }
PodcastUiState(
PodcastUiState.Ready(
podcast = podcast.podcast.asExternalModel().copy(isSubscribed = podcast.isFollowed),
episodes = episodes,
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = PodcastUiState()
initialValue = PodcastUiState.Loading
)

fun toggleSusbcribe(podcast: PodcastInfo) {
Expand Down
Loading

0 comments on commit 8223314

Please sign in to comment.