Composable Function

ListDetailPaneScaffold

A three pane layout that follows the Material guidelines, displaying the provided panes in a canonical list-detail layout.

RevenueCat

RevenueCat

Add subscriptions to your apps in minutes

Ad Get started for free

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,
                )
            }
        },
    )
}

ListDetailPaneScaffoldSampleWithExtraPane

@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,
    )
}