A three pane layout that follows the Material guidelines, displaying the provided panes in a canonical list-detail layout.
ListDetailPaneScaffoldSample
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Preview
@Composable
fun ListDetailPaneScaffoldSample() {
val coroutineScope = rememberCoroutineScope()
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<NavItemData>()
val items = listOf("Item 1", "Item 2", "Item 3")
val selectedItem = scaffoldNavigator.currentDestination?.contentKey
ListDetailPaneScaffold(
directive = scaffoldNavigator.scaffoldDirective,
scaffoldState = scaffoldNavigator.scaffoldState,
listPane = {
AnimatedPane(modifier = Modifier.preferredWidth(200.dp)) {
ListPaneContent(
items = items,
selectedItem = selectedItem,
scaffoldNavigator = scaffoldNavigator,
coroutineScope = coroutineScope,
)
}
},
detailPane = {
AnimatedPane {
DetailPaneContent(
items = items,
selectedItem = selectedItem,
scaffoldNavigator = scaffoldNavigator,
coroutineScope = coroutineScope,
)
}
},
)
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Preview
@Composable
fun ListDetailPaneScaffoldSampleWithExtraPane() {
val coroutineScope = rememberCoroutineScope()
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<NavItemData>()
val items = listOf("Item 1", "Item 2", "Item 3")
val extraItems = listOf("Extra 1", "Extra 2", "Extra 3")
val selectedItem = scaffoldNavigator.currentDestination?.contentKey
ListDetailPaneScaffold(
directive = scaffoldNavigator.scaffoldDirective,
scaffoldState = scaffoldNavigator.scaffoldState,
listPane = {
AnimatedPane(modifier = Modifier.preferredWidth(200.dp)) {
ListPaneContent(
items = items,
selectedItem = selectedItem,
scaffoldNavigator = scaffoldNavigator,
coroutineScope = coroutineScope,
)
}
},
detailPane = {
AnimatedPane {
DetailPaneContent(
items = items,
selectedItem = selectedItem,
scaffoldNavigator = scaffoldNavigator,
hasExtraPane = true,
coroutineScope = coroutineScope,
)
}
},
extraPane = {
AnimatedPane {
ExtraPaneContent(
extraItems = extraItems,
selectedItem = selectedItem,
scaffoldNavigator = scaffoldNavigator,
coroutineScope = coroutineScope,
)
}
},
paneExpansionState =
rememberPaneExpansionState(
keyProvider = scaffoldNavigator.scaffoldValue,
anchors = PaneExpansionAnchors,
initialAnchoredIndex = 1,
),
paneExpansionDragHandle = { state -> PaneExpansionDragHandleSample(state) },
)
}
ListDetailWithNavigation2Sample
@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class)
@Preview
@Composable
fun ListDetailWithNavigation2Sample() {
val welcomeRoute = "welcome"
val listDetailRoute = "listdetail"
val coroutineScope = rememberCoroutineScope()
// `navController` handles navigation outside the ListDetailPaneScaffold,
// and `scaffoldNavigator` handles navigation within it. The "content" of
// the scaffold uses a custom type which tracks the index of the selected item,
// which is passed as a type argument to `rememberListDetailPaneScaffoldNavigator`.
val navController = rememberNavController()
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<NavItemData>()
// Back behavior can be customized based on the needs of the app.
var backBehaviorIndex by rememberSaveable { mutableStateOf(0) }
val backBehaviors =
listOf(
BackNavigationBehavior.PopUntilScaffoldValueChange,
BackNavigationBehavior.PopUntilCurrentDestinationChange,
BackNavigationBehavior.PopUntilContentChange,
BackNavigationBehavior.PopLatest,
)
val backBehavior = backBehaviors[backBehaviorIndex]
val items = listOf("Item 1", "Item 2", "Item 3")
val extraItems = listOf("Extra 1", "Extra 2", "Extra 3")
NavHost(
navController = navController,
startDestination = welcomeRoute,
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) },
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) },
) {
composable(welcomeRoute) {
Scaffold(Modifier.fillMaxSize()) { paddingValues ->
Column(
modifier =
Modifier.verticalScroll(rememberScrollState())
.padding(paddingValues)
.padding(24.dp)
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = "How should the scaffold handle back navigation?",
style = MaterialTheme.typography.headlineMedium,
)
RadioButtonRow(
selected = backBehaviorIndex == 0,
onClick = { backBehaviorIndex = 0 },
text =
"PopUntilScaffoldValueChange - Back navigation forces a change in " +
"which pane(s) is/are shown.",
)
RadioButtonRow(
selected = backBehaviorIndex == 1,
onClick = { backBehaviorIndex = 1 },
text =
"PopUntilCurrentDestinationChange - Back navigation forces a " +
"change in which pane is currently considered \"active\".",
)
RadioButtonRow(
selected = backBehaviorIndex == 2,
onClick = { backBehaviorIndex = 2 },
text =
"PopUntilContentChange - Back navigation forces a change in the " +
"content of any pane or which pane(s) is/are shown.\nNote: this " +
"may result in unintuitive behavior if the device size changes " +
"in the middle of the navigation.",
)
RadioButtonRow(
selected = backBehaviorIndex == 3,
onClick = { backBehaviorIndex = 3 },
text =
"PopLatest - No special back handling.\nNote: this may result in " +
"unintuitive behavior if the device size changes in the middle " +
"of the navigation.",
)
Button(onClick = { navController.navigate(listDetailRoute) }) { Text("Next") }
}
}
}
composable(listDetailRoute) {
val selectedItem = scaffoldNavigator.currentDestination?.contentKey
NavigableListDetailPaneScaffold(
navigator = scaffoldNavigator,
defaultBackBehavior = backBehavior,
listPane = {
AnimatedPane(Modifier.preferredWidth(200.dp)) {
ListPaneContent(
items = items,
selectedItem = selectedItem,
scaffoldNavigator = scaffoldNavigator,
coroutineScope = coroutineScope,
)
}
},
detailPane = {
AnimatedPane {
DetailPaneContent(
items = items,
selectedItem = selectedItem,
scaffoldNavigator = scaffoldNavigator,
hasExtraPane = true,
backBehavior = backBehavior,
coroutineScope = coroutineScope,
)
}
},
extraPane = {
AnimatedPane {
ExtraPaneContent(
extraItems = extraItems,
selectedItem = selectedItem,
scaffoldNavigator = scaffoldNavigator,
backBehavior = backBehavior,
coroutineScope = coroutineScope,
)
}
},
)
}
}
}
PaneExpansionDragHandleSample
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Preview
@Composable
fun ThreePaneScaffoldScope.PaneExpansionDragHandleSample(
state: PaneExpansionState = rememberPaneExpansionState()
) {
val interactionSource = remember { MutableInteractionSource() }
VerticalDragHandle(
modifier =
Modifier.paneExpansionDraggable(
state,
LocalMinimumInteractiveComponentSize.current,
interactionSource,
),
interactionSource = interactionSource,
)
}