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 layout library.

Add the following three, related dependencies to the build.gradle file of your app or module:

Kotlin

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

Groovy

implementation 'androidx.compose.material3.adaptive:adaptive'
implementation 'androidx.compose.material3.adaptive:adaptive-layout'
implementation 'androidx.compose.material3.adaptive:adaptive-navigation'
  • adaptive — Low-level building blocks such as HingeInfo and Posture
  • adaptive-layout — Adaptive layouts such as ListDetailPaneScaffold and SupportingPaneScaffold
  • adaptive-navigation — Composables for navigating within and between panes

Basic usage

Implement ListDetailPaneScaffold as follows:

  1. Use a class that represents the content to be selected. This class should be Parcelable to support saving and restoring the selected list item. Use the kotlin-parcelize plugin to generate the code for you.

    @Parcelize
    class MyItem(val id: Int) : Parcelable

  2. Create a ThreePaneScaffoldNavigator with rememberListDetailPaneScaffoldNavigator and add a BackHandler. This navigator is used to move between the list, detail, and extra panes. By declaring a generic type, the navigator also tracks the state of the scaffold (that is, which MyItem is being displayed). Since this type is parcelable, the state can be saved and restored by the navigator to automatically handle configuration changes. The 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() is true, enabling the BackHandler.

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

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

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

  4. Supply your list pane implementation to the ListDetailPaneScaffold. Use AnimatedPane to apply the default pane animations during navigation. Then use ThreePaneScaffoldNavigator to navigate to the detail pane, ListDetailPaneScaffoldRole.Detail, and display the passed item.

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            AnimatedPane {
                MyList(
                    onItemClick = { item ->
                        // Navigate to the detail pane with the passed item
                        navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, item)
                    }
                )
            }
        },
        // ...
    )

  5. Include your detail pane implementation in ListDetailPaneScaffold. When navigation has completed, currentDestination contains the pane your app has navigated to, including the content displayed in the pane. The content property is the same type specified in the original remember call (MyItem in this example), so you can also access the property for any data that you need to display.

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane =
        // ...
        detailPane = {
            AnimatedPane {
                navigator.currentDestination?.content?.let {
                    MyDetails(it)
                }
            }
        },
    )

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

val navigator = rememberListDetailPaneScaffoldNavigator<MyItem>()

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

ListDetailPaneScaffold(
    directive = navigator.scaffoldDirective,
    value = navigator.scaffoldValue,
    listPane = {
        AnimatedPane {
            MyList(
                onItemClick = { item ->
                    // Navigate to the detail pane with the passed item
                    navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, item)
                },
            )
        }
    },
    detailPane = {
        AnimatedPane {
            // Show the detail pane content if selected item is available
            navigator.currentDestination?.content?.let {
                MyDetails(it)
            }
        }
    },
)