Skip to content

Commit 8223314

Browse files
authored
[Jetcaster] Adding SupportingPaneScaffold to Home (#1303)
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)
2 parents 6058f86 + c89178c commit 8223314

File tree

9 files changed

+291
-148
lines changed

9 files changed

+291
-148
lines changed

Jetcaster/app/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ dependencies {
100100
implementation(libs.androidx.constraintlayout.compose)
101101

102102
implementation(libs.androidx.compose.foundation)
103+
implementation(libs.androidx.compose.ui)
103104
implementation(libs.androidx.compose.material3)
105+
implementation(libs.androidx.compose.material3.adaptive)
106+
implementation(libs.androidx.compose.material3.adaptive.layout)
107+
implementation(libs.androidx.compose.material3.adaptive.navigation)
104108
implementation(libs.androidx.compose.material3.window)
105109
implementation(libs.androidx.compose.material.iconsExtended)
106110
implementation(libs.androidx.compose.ui.tooling.preview)

Jetcaster/app/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,6 @@ fun JetcasterApp(
5656
) {
5757
composable(Screen.Home.route) { backStackEntry ->
5858
Home(
59-
navigateToPodcastDetails = { podcast ->
60-
appState.navigateToPodcastDetails(podcast.uri, backStackEntry)
61-
},
6259
navigateToPlayer = { episode ->
6360
appState.navigateToPlayer(episode.uri, backStackEntry)
6461
}

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

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
package com.example.jetcaster.ui.home
2020

21+
import androidx.activity.compose.BackHandler
2122
import androidx.compose.foundation.ExperimentalFoundationApi
2223
import androidx.compose.foundation.Image
2324
import androidx.compose.foundation.background
@@ -63,6 +64,10 @@ import androidx.compose.material3.TabRow
6364
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
6465
import androidx.compose.material3.Text
6566
import androidx.compose.material3.TopAppBar
67+
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
68+
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffold
69+
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole
70+
import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator
6671
import androidx.compose.runtime.Composable
6772
import androidx.compose.runtime.LaunchedEffect
6873
import androidx.compose.runtime.getValue
@@ -92,6 +97,8 @@ import com.example.jetcaster.core.data.model.PodcastCategoryFilterResult
9297
import com.example.jetcaster.core.data.model.PodcastInfo
9398
import com.example.jetcaster.ui.home.discover.discoverItems
9499
import com.example.jetcaster.ui.home.library.libraryItems
100+
import com.example.jetcaster.ui.podcast.PodcastDetailsScreen
101+
import com.example.jetcaster.ui.podcast.PodcastDetailsViewModel
95102
import com.example.jetcaster.ui.theme.JetcasterTheme
96103
import com.example.jetcaster.util.ToggleFollowPodcastIconButton
97104
import com.example.jetcaster.util.quantityStringResource
@@ -103,30 +110,63 @@ import kotlinx.collections.immutable.PersistentList
103110
import kotlinx.collections.immutable.toPersistentList
104111
import kotlinx.coroutines.launch
105112

113+
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
106114
@Composable
107115
fun Home(
108-
navigateToPodcastDetails: (PodcastInfo) -> Unit,
109116
navigateToPlayer: (EpisodeInfo) -> Unit,
110117
viewModel: HomeViewModel = viewModel()
111118
) {
112119
val viewState by viewModel.state.collectAsStateWithLifecycle()
113-
Surface(Modifier.fillMaxSize()) {
114-
Home(
115-
featuredPodcasts = viewState.featuredPodcasts,
116-
isRefreshing = viewState.refreshing,
117-
homeCategories = viewState.homeCategories,
118-
selectedHomeCategory = viewState.selectedHomeCategory,
119-
filterableCategoriesModel = viewState.filterableCategoriesModel,
120-
podcastCategoryFilterResult = viewState.podcastCategoryFilterResult,
121-
library = viewState.library,
122-
onHomeCategorySelected = viewModel::onHomeCategorySelected,
123-
onCategorySelected = viewModel::onCategorySelected,
124-
onPodcastUnfollowed = viewModel::onPodcastUnfollowed,
125-
navigateToPodcastDetails = navigateToPodcastDetails,
126-
navigateToPlayer = navigateToPlayer,
127-
onTogglePodcastFollowed = viewModel::onTogglePodcastFollowed,
128-
onLibraryPodcastSelected = viewModel::onLibraryPodcastSelected,
129-
onQueueEpisode = viewModel::onQueueEpisode,
120+
val navigator = rememberSupportingPaneScaffoldNavigator<String>(
121+
isDestinationHistoryAware = false
122+
)
123+
BackHandler(enabled = navigator.canNavigateBack()) {
124+
navigator.navigateBack()
125+
}
126+
Surface {
127+
SupportingPaneScaffold(
128+
value = navigator.scaffoldValue,
129+
directive = navigator.scaffoldDirective,
130+
supportingPane = {
131+
val podcastUri = navigator.currentDestination?.content
132+
?: viewState.featuredPodcasts.firstOrNull()?.uri
133+
if (!podcastUri.isNullOrEmpty()) {
134+
val podcastDetailsViewModel = PodcastDetailsViewModel(
135+
podcastUri = podcastUri
136+
)
137+
PodcastDetailsScreen(
138+
viewModel = podcastDetailsViewModel,
139+
navigateToPlayer = navigateToPlayer,
140+
navigateBack = {
141+
if (navigator.canNavigateBack()) {
142+
navigator.navigateBack()
143+
}
144+
}
145+
)
146+
}
147+
},
148+
mainPane = {
149+
Home(
150+
featuredPodcasts = viewState.featuredPodcasts,
151+
isRefreshing = viewState.refreshing,
152+
homeCategories = viewState.homeCategories,
153+
selectedHomeCategory = viewState.selectedHomeCategory,
154+
filterableCategoriesModel = viewState.filterableCategoriesModel,
155+
podcastCategoryFilterResult = viewState.podcastCategoryFilterResult,
156+
library = viewState.library,
157+
onHomeCategorySelected = viewModel::onHomeCategorySelected,
158+
onCategorySelected = viewModel::onCategorySelected,
159+
onPodcastUnfollowed = viewModel::onPodcastUnfollowed,
160+
navigateToPodcastDetails = {
161+
navigator.navigateTo(SupportingPaneScaffoldRole.Supporting, it.uri)
162+
},
163+
navigateToPlayer = navigateToPlayer,
164+
onTogglePodcastFollowed = viewModel::onTogglePodcastFollowed,
165+
onLibraryPodcastSelected = viewModel::onLibraryPodcastSelected,
166+
onQueueEpisode = viewModel::onQueueEpisode,
167+
modifier = Modifier.fillMaxSize()
168+
)
169+
},
130170
modifier = Modifier.fillMaxSize()
131171
)
132172
}

Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/library/Library.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ fun LazyListScope.libraryItems(
6363
onClick = navigateToPlayer,
6464
onQueueEpisode = onQueueEpisode,
6565
modifier = Modifier.fillParentMaxWidth(),
66-
showDivider = index != 0
6766
)
6867
}
6968
}

Jetcaster/app/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import com.example.jetcaster.designsystem.theme.Keyline1
6969
import com.example.jetcaster.ui.home.PreviewEpisodes
7070
import com.example.jetcaster.ui.home.PreviewPodcasts
7171
import com.example.jetcaster.ui.shared.EpisodeListItem
72+
import com.example.jetcaster.ui.shared.Loading
7273
import kotlinx.coroutines.launch
7374

7475
@Composable
@@ -79,15 +80,31 @@ fun PodcastDetailsScreen(
7980
modifier: Modifier = Modifier
8081
) {
8182
val state by viewModel.state.collectAsStateWithLifecycle()
82-
PodcastDetailsScreen(
83-
podcast = state.podcast,
84-
episodes = state.episodes,
85-
toggleSubscribe = viewModel::toggleSusbcribe,
86-
onQueueEpisode = viewModel::onQueueEpisode,
87-
navigateToPlayer = navigateToPlayer,
88-
navigateBack = navigateBack,
89-
modifier = modifier,
90-
)
83+
when (val s = state) {
84+
is PodcastUiState.Loading -> {
85+
PodcastDetailsLoadingScreen(
86+
modifier = Modifier.fillMaxSize()
87+
)
88+
}
89+
is PodcastUiState.Ready -> {
90+
PodcastDetailsScreen(
91+
podcast = s.podcast,
92+
episodes = s.episodes,
93+
toggleSubscribe = viewModel::toggleSusbcribe,
94+
onQueueEpisode = viewModel::onQueueEpisode,
95+
navigateToPlayer = navigateToPlayer,
96+
navigateBack = navigateBack,
97+
modifier = modifier,
98+
)
99+
}
100+
}
101+
}
102+
103+
@Composable
104+
private fun PodcastDetailsLoadingScreen(
105+
modifier: Modifier = Modifier
106+
) {
107+
Loading(modifier = modifier)
91108
}
92109

93110
@Composable

Jetcaster/app/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,13 @@ import kotlinx.coroutines.flow.combine
3838
import kotlinx.coroutines.flow.stateIn
3939
import kotlinx.coroutines.launch
4040

41-
data class PodcastUiState(
42-
val podcast: PodcastInfo = PodcastInfo(),
43-
val episodes: List<EpisodeInfo> = emptyList()
44-
)
41+
sealed interface PodcastUiState {
42+
data object Loading : PodcastUiState
43+
data class Ready(
44+
val podcast: PodcastInfo,
45+
val episodes: List<EpisodeInfo>,
46+
) : PodcastUiState
47+
}
4548

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

56-
private val podcastUri: String =
57-
Uri.decode(savedStateHandle.get<String>(Screen.ARG_PODCAST_URI)!!)
59+
constructor(
60+
episodeStore: EpisodeStore = Graph.episodeStore,
61+
episodePlayer: EpisodePlayer = Graph.episodePlayer,
62+
podcastStore: PodcastStore = Graph.podcastStore,
63+
savedStateHandle: SavedStateHandle
64+
) : this(
65+
episodeStore = episodeStore,
66+
episodePlayer = episodePlayer,
67+
podcastStore = podcastStore,
68+
podcastUri = Uri.decode(savedStateHandle.get<String>(Screen.ARG_PODCAST_URI)!!)
69+
)
5870

5971
val state: StateFlow<PodcastUiState> =
6072
combine(
6173
podcastStore.podcastWithExtraInfo(podcastUri),
6274
episodeStore.episodesInPodcast(podcastUri)
6375
) { podcast, episodeToPodcasts ->
6476
val episodes = episodeToPodcasts.map { it.episode.asExternalModel() }
65-
PodcastUiState(
77+
PodcastUiState.Ready(
6678
podcast = podcast.podcast.asExternalModel().copy(isSubscribed = podcast.isFollowed),
6779
episodes = episodes,
6880
)
6981
}.stateIn(
7082
scope = viewModelScope,
7183
started = SharingStarted.WhileSubscribed(5_000),
72-
initialValue = PodcastUiState()
84+
initialValue = PodcastUiState.Loading
7385
)
7486

7587
fun toggleSusbcribe(podcast: PodcastInfo) {

0 commit comments

Comments
 (0)