From abf6da37a96000553487a2f1e83dd417ef64db99 Mon Sep 17 00:00:00 2001 From: Gerard Paligot Date: Sun, 21 Jan 2024 14:43:30 +0100 Subject: [PATCH] feat(speakers): support adaptive layout. --- .../devfest/android/theme/MainNavigation.kt | 12 ++-- .../speakers-feature/build.gradle.kts | 1 + .../m3/speakers/feature/SpeakerAdaptive.kt | 57 +++++++++++++++ .../feature/SpeakerDetailOrientableVM.kt | 69 +++++++++---------- .../speakers/feature/SpeakersListCompactVM.kt | 26 +++---- .../screens/SpeakerDetailOrientable.kt | 10 ++- ...rticalScreen.kt => SpeakerDetailScreen.kt} | 7 +- .../m3/speakers/screens/SpeakersListScreen.kt | 6 +- 8 files changed, 115 insertions(+), 73 deletions(-) create mode 100644 theme-m3/speakers/speakers-feature/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/feature/SpeakerAdaptive.kt rename theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/{SpeakerDetailVerticalScreen.kt => SpeakerDetailScreen.kt} (93%) diff --git a/theme-m3/main/main/src/main/kotlin/org/gdglille/devfest/android/theme/MainNavigation.kt b/theme-m3/main/main/src/main/kotlin/org/gdglille/devfest/android/theme/MainNavigation.kt index 891d6a830..629d018a0 100644 --- a/theme-m3/main/main/src/main/kotlin/org/gdglille/devfest/android/theme/MainNavigation.kt +++ b/theme-m3/main/main/src/main/kotlin/org/gdglille/devfest/android/theme/MainNavigation.kt @@ -1,5 +1,6 @@ package org.gdglille.devfest.android.theme +import android.content.res.Configuration import androidx.compose.material3.Icon import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo @@ -38,8 +39,8 @@ import org.gdglille.devfest.android.theme.m3.partners.feature.PartnersListCompac import org.gdglille.devfest.android.theme.m3.schedules.feature.AgendaFiltersCompactVM import org.gdglille.devfest.android.theme.m3.schedules.feature.ScheduleDetailOrientableVM import org.gdglille.devfest.android.theme.m3.schedules.feature.ScheduleGridAdaptive +import org.gdglille.devfest.android.theme.m3.speakers.feature.SpeakerAdaptive import org.gdglille.devfest.android.theme.m3.speakers.feature.SpeakerDetailOrientableVM -import org.gdglille.devfest.android.theme.m3.speakers.feature.SpeakersListCompactVM import org.gdglille.devfest.android.theme.m3.style.appbars.iconColor import org.gdglille.devfest.models.ui.ExportNetworkingUi import org.gdglille.devfest.models.ui.VCardModel @@ -184,8 +185,10 @@ fun MainNavigation( ) } composable(Screen.SpeakerList.route) { - SpeakersListCompactVM( - onSpeakerClicked = { navController.navigate(Screen.Speaker.route(it)) } + SpeakerAdaptive( + showBackInDetail = adaptiveInfo.windowSizeClass.widthSizeClass.isCompat, + onTalkClicked = { navController.navigate(Screen.Schedule.route(it)) }, + onLinkClicked = { launchUrl(it) } ) } composable( @@ -196,7 +199,8 @@ fun MainNavigation( speakerId = it.arguments?.getString("speakerId")!!, onTalkClicked = { navController.navigate(Screen.Schedule.route(it)) }, onLinkClicked = { launchUrl(it) }, - onBackClicked = { navController.popBackStack() } + navigationIcon = { Back { navController.popBackStack() } }, + isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE ) } composable(Screen.MyProfile.route) { diff --git a/theme-m3/speakers/speakers-feature/build.gradle.kts b/theme-m3/speakers/speakers-feature/build.gradle.kts index efc66fbac..66842f29a 100644 --- a/theme-m3/speakers/speakers-feature/build.gradle.kts +++ b/theme-m3/speakers/speakers-feature/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material3.adaptive) implementation(libs.androidx.compose.tooling) implementation(libs.androidx.compose.lifecycle) implementation(libs.androidx.lifecycle.vm) diff --git a/theme-m3/speakers/speakers-feature/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/feature/SpeakerAdaptive.kt b/theme-m3/speakers/speakers-feature/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/feature/SpeakerAdaptive.kt new file mode 100644 index 000000000..d4930ecf1 --- /dev/null +++ b/theme-m3/speakers/speakers-feature/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/feature/SpeakerAdaptive.kt @@ -0,0 +1,57 @@ +package org.gdglille.devfest.android.theme.m3.speakers.feature + +import androidx.compose.material3.adaptive.AnimatedPane +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.rememberListDetailPaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun SpeakerAdaptive( + showBackInDetail: Boolean, + onTalkClicked: (id: String) -> Unit, + onLinkClicked: (url: String) -> Unit, + modifier: Modifier = Modifier +) { + val navigator = rememberListDetailPaneScaffoldNavigator() + var selectedItem: String? by rememberSaveable { mutableStateOf(null) } + ListDetailPaneScaffold( + scaffoldState = navigator.scaffoldState, + modifier = modifier, + listPane = { + AnimatedPane(Modifier) { + SpeakersListCompactVM( + onSpeakerClicked = { + selectedItem = it + navigator.navigateTo(ListDetailPaneScaffoldRole.Detail) + } + ) + } + }, + detailPane = { + AnimatedPane(modifier = Modifier) { + selectedItem?.let { item -> + SpeakerDetailOrientableVM( + speakerId = item, + onTalkClicked = onTalkClicked, + onLinkClicked = onLinkClicked, + navigationIcon = if (showBackInDetail) { + @Composable { Back { + if (navigator.canNavigateBack()) { + navigator.navigateBack() + } + } } + } else null + ) + } + } + } + ) +} diff --git a/theme-m3/speakers/speakers-feature/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/feature/SpeakerDetailOrientableVM.kt b/theme-m3/speakers/speakers-feature/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/feature/SpeakerDetailOrientableVM.kt index 5f59c2ef8..0a4203eb2 100644 --- a/theme-m3/speakers/speakers-feature/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/feature/SpeakerDetailOrientableVM.kt +++ b/theme-m3/speakers/speakers-feature/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/feature/SpeakerDetailOrientableVM.kt @@ -1,65 +1,58 @@ package org.gdglille.devfest.android.theme.m3.speakers.feature -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import org.gdglille.devfest.android.theme.m3.speakers.screens.SpeakerDetailOrientable import org.gdglille.devfest.android.theme.m3.style.R -import org.gdglille.devfest.android.theme.m3.style.appbars.TopAppBar +import org.gdglille.devfest.android.theme.m3.style.Scaffold +import org.gdglille.devfest.android.theme.m3.style.appbars.AppBarIcons import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalFoundationApi::class) @Composable fun SpeakerDetailOrientableVM( speakerId: String, onTalkClicked: (id: String) -> Unit, onLinkClicked: (url: String) -> Unit, - onBackClicked: () -> Unit, modifier: Modifier = Modifier, - viewModel: SpeakerDetailViewModel = koinViewModel(parameters = { parametersOf(speakerId) }) + navigationIcon: @Composable (AppBarIcons.() -> Unit)? = null, + isLandscape: Boolean = false, + viewModel: SpeakerDetailViewModel = koinViewModel(key = speakerId, parameters = { parametersOf(speakerId) }) ) { val context = LocalContext.current val uiState = viewModel.uiState.collectAsState() - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) Scaffold( - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopAppBar( - title = stringResource(id = R.string.screen_speaker_detail), - navigationIcon = { Back(onClick = onBackClicked) }, - scrollBehavior = scrollBehavior + title = stringResource(id = R.string.screen_speaker_detail), + navigationIcon = navigationIcon, + modifier = modifier + ) { + when (uiState.value) { + is SpeakerUiState.Loading -> SpeakerDetailOrientable( + speaker = (uiState.value as SpeakerUiState.Loading).speaker, + contentPadding = it, + onTalkClicked = {}, + onFavoriteClicked = {}, + onLinkClicked = {}, + isLandscape = isLandscape ) - }, - content = { - when (uiState.value) { - is SpeakerUiState.Loading -> SpeakerDetailOrientable( - speaker = (uiState.value as SpeakerUiState.Loading).speaker, - contentPadding = it, - onTalkClicked = {}, - onFavoriteClicked = {}, - onLinkClicked = {} - ) - is SpeakerUiState.Failure -> Text(text = stringResource(id = R.string.text_error)) - is SpeakerUiState.Success -> SpeakerDetailOrientable( - speaker = (uiState.value as SpeakerUiState.Success).speaker, - contentPadding = it, - onTalkClicked = onTalkClicked, - onFavoriteClicked = { - viewModel.markAsFavorite(context, it) - }, - onLinkClicked = onLinkClicked, - ) - } + is SpeakerUiState.Failure -> Text(text = stringResource(id = R.string.text_error)) + is SpeakerUiState.Success -> SpeakerDetailOrientable( + speaker = (uiState.value as SpeakerUiState.Success).speaker, + contentPadding = it, + onTalkClicked = onTalkClicked, + onFavoriteClicked = { + viewModel.markAsFavorite(context, it) + }, + onLinkClicked = onLinkClicked, + isLandscape = isLandscape + ) } - ) + } } diff --git a/theme-m3/speakers/speakers-feature/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/feature/SpeakersListCompactVM.kt b/theme-m3/speakers/speakers-feature/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/feature/SpeakersListCompactVM.kt index 863316a93..bbb7e2446 100644 --- a/theme-m3/speakers/speakers-feature/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/feature/SpeakersListCompactVM.kt +++ b/theme-m3/speakers/speakers-feature/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/feature/SpeakersListCompactVM.kt @@ -1,6 +1,5 @@ package org.gdglille.devfest.android.theme.m3.speakers.feature -import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.rememberLazyGridState @@ -8,16 +7,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import org.gdglille.devfest.android.theme.m3.speakers.screens.SpeakersListScreen import org.gdglille.devfest.android.theme.m3.style.R import org.gdglille.devfest.android.theme.m3.style.Scaffold import org.koin.androidx.compose.koinViewModel -private const val ColumnCountLandscape = 4 -private const val ColumnCountPortrait = 2 - @OptIn(ExperimentalFoundationApi::class) @Composable fun SpeakersListCompactVM( @@ -25,35 +20,30 @@ fun SpeakersListCompactVM( modifier: Modifier = Modifier, viewModel: SpeakersListViewModel = koinViewModel() ) { - val configuration = LocalConfiguration.current val state = rememberLazyGridState() val uiState = viewModel.uiState.collectAsState() - val columnCount = - if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) ColumnCountLandscape - else ColumnCountPortrait Scaffold( title = stringResource(id = R.string.screen_speakers), - modifier = modifier + modifier = modifier, + hasScrollBehavior = false ) { when (uiState.value) { is SpeakersUiState.Loading -> SpeakersListScreen( speakers = (uiState.value as SpeakersUiState.Loading).speakers, - columnCount = columnCount, - state = state, onSpeakerClicked = onSpeakerClicked, - isLoading = true, - modifier = Modifier.padding(it) + modifier = Modifier.padding(it), + state = state, + isLoading = true ) is SpeakersUiState.Failure -> Text(text = stringResource(id = R.string.text_error)) is SpeakersUiState.Success -> { SpeakersListScreen( speakers = (uiState.value as SpeakersUiState.Success).speakers, - columnCount = columnCount, - state = state, onSpeakerClicked = onSpeakerClicked, - isLoading = false, - modifier = Modifier.padding(it) + modifier = Modifier.padding(top = it.calculateTopPadding()), + state = state, + isLoading = false ) } } diff --git a/theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakerDetailOrientable.kt b/theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakerDetailOrientable.kt index 51284be85..3162b136a 100644 --- a/theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakerDetailOrientable.kt +++ b/theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakerDetailOrientable.kt @@ -1,6 +1,5 @@ package org.gdglille.devfest.android.theme.m3.speakers.screens -import android.content.res.Configuration import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -8,7 +7,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp import org.gdglille.devfest.models.ui.SpeakerUi import org.gdglille.devfest.models.ui.TalkItemUi @@ -21,11 +19,11 @@ fun SpeakerDetailOrientable( onLinkClicked: (url: String) -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), + isLandscape: Boolean = false, isLoading: Boolean = false ) { val state = rememberLazyListState() - val orientation = LocalConfiguration.current - if (orientation.orientation == Configuration.ORIENTATION_LANDSCAPE) { + if (isLandscape) { Row( verticalAlignment = Alignment.Top, modifier = modifier.padding(contentPadding) @@ -35,7 +33,7 @@ fun SpeakerDetailOrientable( isLoading = isLoading, modifier = Modifier.weight(1f) ) - SpeakerDetailVerticalScreen( + SpeakerDetailScreen( speaker = speaker, onTalkClicked = onTalkClicked, onFavoriteClicked = onFavoriteClicked, @@ -47,7 +45,7 @@ fun SpeakerDetailOrientable( ) } } else { - SpeakerDetailVerticalScreen( + SpeakerDetailScreen( speaker = speaker, onTalkClicked = onTalkClicked, onFavoriteClicked = onFavoriteClicked, diff --git a/theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakerDetailVerticalScreen.kt b/theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakerDetailScreen.kt similarity index 93% rename from theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakerDetailVerticalScreen.kt rename to theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakerDetailScreen.kt index b874c2454..43c165838 100644 --- a/theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakerDetailVerticalScreen.kt +++ b/theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakerDetailScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -22,7 +21,7 @@ import org.gdglille.devfest.models.ui.SpeakerUi import org.gdglille.devfest.models.ui.TalkItemUi @Composable -fun SpeakerDetailVerticalScreen( +fun SpeakerDetailScreen( speaker: SpeakerUi, onTalkClicked: (id: String) -> Unit, onFavoriteClicked: (TalkItemUi) -> Unit, @@ -65,10 +64,10 @@ fun SpeakerDetailVerticalScreen( @Suppress("UnusedPrivateMember") @ThemedPreviews @Composable -private fun SpeakerDetailVerticalScreenPreview() { +private fun SpeakerDetailScreenPreview() { Conferences4HallTheme { Scaffold { - SpeakerDetailVerticalScreen( + SpeakerDetailScreen( speaker = SpeakerUi.fake, onTalkClicked = {}, onFavoriteClicked = {}, diff --git a/theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakersListScreen.kt b/theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakersListScreen.kt index 9d5c68734..d959c3720 100644 --- a/theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakersListScreen.kt +++ b/theme-m3/speakers/speakers-screens/src/main/kotlin/org/gdglille/devfest/android/theme/m3/speakers/screens/SpeakersListScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import org.gdglille.devfest.android.theme.m3.style.Conferences4HallTheme @@ -26,16 +27,15 @@ fun SpeakersListScreen( onSpeakerClicked: (id: String) -> Unit, modifier: Modifier = Modifier, state: LazyGridState = rememberLazyGridState(), - columnCount: Int = 2, isLoading: Boolean = false, ) { LazyVerticalGrid( - columns = GridCells.Fixed(count = columnCount), + columns = GridCells.Adaptive(minSize = 150.dp), modifier = modifier.fillMaxWidth(), state = state, verticalArrangement = Arrangement.spacedBy(SpacingTokens.MediumSpacing.toDp()), horizontalArrangement = Arrangement.spacedBy(SpacingTokens.MediumSpacing.toDp()), - contentPadding = PaddingValues(SpacingTokens.LargeSpacing.toDp()), + contentPadding = PaddingValues(vertical = SpacingTokens.ExtraLargeSpacing.toDp()), content = { items(speakers.toList(), key = { it.id }) { LargeSpeakerItem(