Build a list-detail layout

List-detail is a UI pattern that consists of a dual-pane layout where one pane presents a list of items and another pane displays the details of items selected from the list.

The pattern is particularly useful for applications that provide in-depth information about elements of large collections, for example, an email client that has a list of emails and the detailed content of each email message. List-detail can also be used for less critical paths such as dividing app preferences into a list of categories with the preferences for each category in the detail pane.

Implement UI pattern with ListDetailPaneScaffold

ListDetailPaneScaffold is a composable that simplifies the implementation of the list-detail pattern in your app. A list-detail scaffold can consist of up to three panes: a list pane, a detail pane, and an optional extra pane. The scaffold handles screen space calculations. When sufficient screen size is available, the detail pane is displayed alongside the list pane. On small screen sizes, the scaffold automatically switches to displaying either the list or detail pane full screen.

A detail pane shown alongside the list page.
Figure 1. When enough screen size is available, the detail pane is shown alongside the list pane.
After an item is selected, the detail pane takes over the whole screen.
Figure 2. When screen size is limited, the detail pane (since an item has been selected) takes over the whole space.

Declare dependencies

ListDetailPaneScaffold is part of the Material 3 adaptive library. Add a dependency for the library in the build.gradle file for your app or module:

implementation("androidx.compose.material3.adaptive:adaptive:1.0.0-alpha07")
implementation("androidx.compose.material3.adaptive:adaptive-layout:1.0.0-alpha07")
implementation("androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-alpha07")

Basic usage

The following illustrates the basic usage of ListDetailPaneScaffold:

  1. Store the currently selected item from the list in a mutable state variable. The variable holds the item to be displayed in the detail pane. Typically, you'd want to initialize the currently selected item with null, indicating that no selection was made yet.

    class MyItem(val id: Int) {
        companion object {
            val Saver: Saver<MyItem?, Int> = Saver(
                { it?.id },
                ::MyItem,
            )
        }
    }

    var selectedItem: MyItem? by rememberSaveable(stateSaver = MyItem.Saver) {
        mutableStateOf(null)
    }

  2. Create the ThreePaneScaffoldNavigator with rememberListDetailPaneScaffoldNavigator and add a BackHandler. This navigator is used to move between the list, detail, and extra panes, and provide the state to the scaffold. The added BackHandler provides support for navigating back using the system back gesture or button. The expected behavior of the back button for a ListDetailPaneScaffold depends on the window size and current scaffold value. If the ListDetailPaneScaffold can support going back with the current state, then canNavigateBack() will be true, enabling the BackHandler.

    val navigator = rememberListDetailPaneScaffoldNavigator<Nothing>()
    
    BackHandler(navigator.canNavigateBack()) {
        navigator.navigateBack()
    }

  3. Pass the scaffoldState from the navigator you created to the ListDetailPaneScaffold composable.

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        // ...
    )

  4. Supply your list pane implementation to the ListDetailPaneScaffold. Ensure that your implementation includes a callback argument for capturing the newly selected item. When this callback is triggered, update the selectedItem state variable and use ThreePaneScaffoldNavigator to display the detail pane (ListDetailPaneScaffoldRole.Detail).

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            AnimatedPane(Modifier) {
                MyList(
                    onItemClick = { id ->
                        // Set current item
                        selectedItem = id
                        // Switch focus to detail pane
                        navigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
                    }
                )
            }
        },
        // ...
    )

  5. Include your detail pane implementation in ListDetailPaneScaffold. Display the detail content only if the selectedItem is not null.

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane =
        // ...
        detailPane = {
            AnimatedPane(Modifier) {
                selectedItem?.let { item ->
                    MyDetails(item)
                }
            }
        },
    )

After implementing the above steps, your code should look similar to this:

// Currently selected item
var selectedItem: MyItem? by rememberSaveable(stateSaver = MyItem.Saver) {
    mutableStateOf(null)
}

// Create the ListDetailPaneScaffoldState
val navigator = rememberListDetailPaneScaffoldNavigator<Nothing>()

BackHandler(navigator.canNavigateBack()) {
    navigator.navigateBack()
}

ListDetailPaneScaffold(
    directive = navigator.scaffoldDirective,
    value = navigator.scaffoldValue,
    listPane = {
        AnimatedPane(Modifier) {
            MyList(
                onItemClick = { id ->
                    // Set current item
                    selectedItem = id
                    // Display the detail pane
                    navigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
                },
            )
        }
    },
    detailPane = {
        AnimatedPane(Modifier) {
            // Show the detail pane content if selected item is available
            selectedItem?.let { item ->
                MyDetails(item)
            }
        }
    },
)