A ListDetailSceneStrategy supports arranging NavEntrys into an adaptive ListDetailPaneScaffold. By using listPane, detailPane, or extraPane in a NavEntry's metadata, entries can be assigned as belonging to a list pane, detail pane, or extra pane. These panes will be displayed together if the window size is sufficiently large, and will automatically adapt if the window size changes, for example, on a foldable device.

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
import androidx.compose.material3.adaptive.navigation3.LocalListDetailSceneScope
import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.onClick
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay

val backStack = rememberNavBackStack(HomeKey)
val sceneStrategy = rememberListDetailSceneStrategy<Any>()

val items = listOf("Item 1", "Item 2", "Item 3")
val extraItems = listOf("Extra 1", "Extra 2", "Extra 3")

val selectedIndex =
    backStack.lastOrNull()?.let {
        when (it) {
            is DetailKey -> it.index
            is ExtraKey -> it.index
            else -> null
        }
    }

NavDisplay(
    backStack = backStack,
    modifier = Modifier.fillMaxSize(),
    sceneStrategy = sceneStrategy,
    entryProvider =
        entryProvider {
            entry<HomeKey> {
                Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                    Button(
                        onClick = { if (backStack.last() != ListKey) backStack.add(ListKey) }
                    ) {
                        Text("Go to list")
                    }
                }
            }

            entry<ListKey>(
                metadata =
                    ListDetailSceneStrategy.listPane(
                        detailPlaceholder = {
                            DetailPaneContent(selectedItem = null, onShowExtra = {})
                        }
                    )
            ) {
                ListPaneContent(
                    items = items,
                    selectedIndex = selectedIndex,
                    onItemClick = { index ->
                        val dest = DetailKey(index)
                        if (backStack.last() != dest) backStack.add(dest)
                    },
                )
            }

            entry<DetailKey>(metadata = ListDetailSceneStrategy.detailPane()) {
                val scaffoldSceneScope = LocalListDetailSceneScope.current
                DetailPaneContent(
                    selectedItem = selectedIndex?.let { items[it] },
                    onShowExtra = {
                        val dest = ExtraKey(selectedIndex!!)
                        if (backStack.last() != dest) backStack.add(dest)
                    },
                    backButton =
                        if (scaffoldSceneScope == null) {
                            // Only show back button in a single-pane context
                            { BackButton(onClick = { backStack.removeLastOrNull() }) }
                        } else null,
                )
            }

            entry<ExtraKey>(metadata = ListDetailSceneStrategy.extraPane()) {
                val scaffoldSceneScope = LocalListDetailSceneScope.current
                ExtraPaneContent(
                    item = extraItems[selectedIndex!!],
                    backButton =
                        if (scaffoldSceneScope == null) {
                            // Only show back button in a single-pane context
                            { BackButton(onClick = { backStack.removeLastOrNull() }) }
                        } else null,
                )
            }
        },
)

Summary

Public companion functions

Map<StringAny>
detailPane(sceneKey: Any)

Constructs metadata to mark a NavEntry as belonging to a detail pane within a ListDetailPaneScaffold.

Cmn
Map<StringAny>
extraPane(sceneKey: Any)

Constructs metadata to mark a NavEntry as belonging to an extra pane within a ListDetailPaneScaffold.

Cmn
Map<StringAny>
listPane(sceneKey: Any, detailPlaceholder: @Composable ThreePaneScaffoldScope.() -> Unit)

Constructs metadata to mark a NavEntry as belonging to a list pane within a ListDetailPaneScaffold.

Cmn
Map<StringAny>
paneAnimation(
    enterTransition: EnterTransition?,
    exitTransition: ExitTransition?,
    boundsAnimationSpec: FiniteAnimationSpec<IntRect>?
)

Constructs metadata to customize the animation of panes within a list-detail scaffold.

Cmn
Map<StringAny>
preferredPaneSize(width: Dp, height: Dp)

Constructs metadata to set the preferred size of a pane within a list-detail scaffold, defined in Dps.

Cmn
Map<StringAny>
preferredPaneSize(width: Float, height: Float)

Constructs metadata to set the preferred size of a pane within a list-detail scaffold, defined as a fraction of the total scaffold size.

Cmn

Public constructors

<T : Any> ListDetailSceneStrategy(
    shouldHandleSinglePaneLayout: Boolean,
    backNavigationBehavior: BackNavigationBehavior,
    directive: PaneScaffoldDirective,
    adaptStrategies: ThreePaneScaffoldAdaptStrategies,
    paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)?,
    paneExpansionState: PaneExpansionState?
)
Cmn

Public functions

open Scene<T>?

Given a SceneStrategyScope, calculate whether this SceneStrategy should take on the task of rendering one or more of the entries in the scope.

Cmn

Public properties

ThreePaneScaffoldAdaptStrategies

adaptation strategies of each pane, which denotes how each pane should be adapted if they can't fit on screen in the PaneAdaptedValue.Expanded state.

Cmn
BackNavigationBehavior

the behavior describing which backstack entries may be skipped during the back navigation.

Cmn
PaneScaffoldDirective

The top-level directives about how the list-detail scaffold should arrange its panes.

Cmn
(@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)?

when two panes are displayed side-by-side, a non-null drag handle allows users to resize the panes and change the pane expansion state.

Cmn
PaneExpansionState?

the state object of pane expansion.

Cmn
Boolean

whether ListDetailSceneStrategy should apply when only a single pane is displayed.

Cmn

Inherited functions

From androidx.navigation3.scene.SceneStrategy
open infix SceneStrategy<T>
then(sceneStrategy: SceneStrategy<T>)

Chains this SceneStrategy with another sceneStrategy to return a combined SceneStrategy.

Cmn

Public companion functions

detailPane

fun detailPane(sceneKey: Any = Unit): Map<StringAny>

Constructs metadata to mark a NavEntry as belonging to a detail pane within a ListDetailPaneScaffold.

Parameters
sceneKey: Any = Unit

the key to distinguish the scene of the list-detail scaffold, in case multiple list-detail scaffolds are supported within the same NavDisplay.

extraPane

fun extraPane(sceneKey: Any = Unit): Map<StringAny>

Constructs metadata to mark a NavEntry as belonging to an extra pane within a ListDetailPaneScaffold.

Parameters
sceneKey: Any = Unit

the key to distinguish the scene of the list-detail scaffold, in case multiple list-detail scaffolds are supported within the same NavDisplay.

listPane

fun listPane(sceneKey: Any = Unit, detailPlaceholder: @Composable ThreePaneScaffoldScope.() -> Unit = {}): Map<StringAny>

Constructs metadata to mark a NavEntry as belonging to a list pane within a ListDetailPaneScaffold.

Parameters
sceneKey: Any = Unit

the key to distinguish the scene of the list-detail scaffold, in case multiple list-detail scaffolds are supported within the same NavDisplay.

detailPlaceholder: @Composable ThreePaneScaffoldScope.() -> Unit = {}

composable content to display in the detail pane in case there is no other NavEntry representing a detail pane in the backstack. Note that this content does not receive the same scoping mechanisms as a full-fledged NavEntry.

paneAnimation

fun paneAnimation(
    enterTransition: EnterTransition? = null,
    exitTransition: ExitTransition? = null,
    boundsAnimationSpec: FiniteAnimationSpec<IntRect>? = null
): Map<StringAny>

Constructs metadata to customize the animation of panes within a list-detail scaffold.

If the value is null or unset, the default motions defined in PaneMotionDefaults will be used instead.

Parameters
enterTransition: EnterTransition? = null

The EnterTransition used to animate the pane in.

exitTransition: ExitTransition? = null

The ExitTransition used to animate the pane out.

boundsAnimationSpec: FiniteAnimationSpec<IntRect>? = null

The FiniteAnimationSpec used to animate the bounds of the pane when it remains showing but changes its size and/or position.

preferredPaneSize

fun preferredPaneSize(width: Dp = Dp.Unspecified, height: Dp = Dp.Unspecified): Map<StringAny>

Constructs metadata to set the preferred size of a pane within a list-detail scaffold, defined in Dps.

If the value is unset or set to Dp.Unspecified, defaultPanePreferredWidth and defaultPanePreferredHeight from directive will be used instead.

Parameters
width: Dp = Dp.Unspecified

the preferred width of the pane, defined in Dp. The implementation will try to respect this value when the pane is rendered as a fixed pane. Note that the preferred width may be ignored when this pane has higher priority than the other panes so it is forced to fill the available width, or if the pane needs to shrink or expand to avoid intersecting with the hinge areas.

height: Dp = Dp.Unspecified

the preferred height of the pane, defined in Dp. The implementation will try to respect this value when the pane is rendered in a PaneAdaptedValue.Reflowed or PaneAdaptedValue.Levitated state. Note that the preferred height may be ignored when the pane is expanded to stretch the available height, or if the pane needs to shrink or expand to avoid intersecting with the hinge areas.

preferredPaneSize

fun preferredPaneSize(width: Float = Float.NaN, height: Float = Float.NaN): Map<StringAny>

Constructs metadata to set the preferred size of a pane within a list-detail scaffold, defined as a fraction of the total scaffold size.

If the value is unset or set to Dp.Unspecified, defaultPanePreferredWidth and defaultPanePreferredHeight from directive will be used instead.

Parameters
width: Float = Float.NaN

the preferred width of the pane, defined as a fraction from 0.0 to 1.0 of the total scaffold width. The implementation will try to respect this value when the pane is rendered as a fixed pane. Note that the preferred width may be ignored when this pane has higher priority than the other panes so it is forced to fill the available width, or if the pane needs to shrink or expand to avoid intersecting with the hinge areas.

height: Float = Float.NaN

the preferred height of the pane, defined as a fraction from 0.0 to 1.0 of the total scaffold height. The implementation will try to respect this value when the pane is rendered in a PaneAdaptedValue.Reflowed or PaneAdaptedValue.Levitated state. Note that the preferred height may be ignored when the pane is expanded to stretch the available height, or if the pane needs to shrink or expand to avoid intersecting with the hinge areas.

Public constructors

ListDetailSceneStrategy

<T : Any> ListDetailSceneStrategy(
    shouldHandleSinglePaneLayout: Boolean,
    backNavigationBehavior: BackNavigationBehavior,
    directive: PaneScaffoldDirective,
    adaptStrategies: ThreePaneScaffoldAdaptStrategies,
    paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)?,
    paneExpansionState: PaneExpansionState?
)
Parameters
shouldHandleSinglePaneLayout: Boolean

whether ListDetailSceneStrategy should apply when only a single pane is displayed. By default, this is false and instead yields to the next SceneStrategy in the chain. If true, single pane layouts will instead be handled internally by the Material adaptive scaffold instead of the Navigation 3 system.

backNavigationBehavior: BackNavigationBehavior

the behavior describing which backstack entries may be skipped during the back navigation. See BackNavigationBehavior.

directive: PaneScaffoldDirective

The top-level directives about how the list-detail scaffold should arrange its panes.

adaptStrategies: ThreePaneScaffoldAdaptStrategies

adaptation strategies of each pane, which denotes how each pane should be adapted if they can't fit on screen in the PaneAdaptedValue.Expanded state. It is recommended to use ListDetailPaneScaffoldDefaults.adaptStrategies as a default, but custom ThreePaneScaffoldAdaptStrategies are supported as well.

paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)?

when two panes are displayed side-by-side, a non-null drag handle allows users to resize the panes and change the pane expansion state.

paneExpansionState: PaneExpansionState?

the state object of pane expansion. If this is null but a paneExpansionDragHandle is provided, a default implementation will be created.

Public functions

open fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>?

Given a SceneStrategyScope, calculate whether this SceneStrategy should take on the task of rendering one or more of the entries in the scope.

By returning a non-null Scene, your Scene takes on the responsibility of rendering the set of entries you declare in Scene.entries. If you return null, the next available SceneStrategy will be called.

Parameters
entries: List<NavEntry<T>>

The entries on the back stack that should be considered valid to render via a returned Scene.

Public properties

adaptStrategies

val adaptStrategiesThreePaneScaffoldAdaptStrategies

adaptation strategies of each pane, which denotes how each pane should be adapted if they can't fit on screen in the PaneAdaptedValue.Expanded state. It is recommended to use ListDetailPaneScaffoldDefaults.adaptStrategies as a default, but custom ThreePaneScaffoldAdaptStrategies are supported as well.

backNavigationBehavior

val backNavigationBehaviorBackNavigationBehavior

the behavior describing which backstack entries may be skipped during the back navigation. See BackNavigationBehavior.

directive

val directivePaneScaffoldDirective

The top-level directives about how the list-detail scaffold should arrange its panes.

paneExpansionDragHandle

val paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)?

when two panes are displayed side-by-side, a non-null drag handle allows users to resize the panes and change the pane expansion state.

paneExpansionState

val paneExpansionStatePaneExpansionState?

the state object of pane expansion. If this is null but a paneExpansionDragHandle is provided, a default implementation will be created.

shouldHandleSinglePaneLayout

val shouldHandleSinglePaneLayoutBoolean

whether ListDetailSceneStrategy should apply when only a single pane is displayed. By default, this is false and instead yields to the next SceneStrategy in the chain. If true, single pane layouts will instead be handled internally by the Material adaptive scaffold instead of the Navigation 3 system.