Build a List-Detail Layout with Compose Adaptive APIs

Quick answer: For a real list-detail flow in a Navigation 3 Compose app, use Material 3 Adaptive’s
ListDetailSceneStrategy. Mark list and detailNavEntrys with pane metadata, add the strategy toNavDisplay, and keep navigating by adding and removing typed keys from the back stack. On a compact window the flow remains one destination at a time; when enough space is available, the same back stack can render the list and selected detail together.
List-detail is a task pattern, not simply two columns. It fits inboxes, note collections, catalogues, settings with rich detail, and customer or project records: people scan a collection, choose an item, and work with its details. A wide layout can retain the collection as context, while a compact layout preserves focus and familiar Back behavior.
The current Navigation 3 integration avoids manually synchronizing a LazyColumn, a selectedId, pane visibility, and a second navigation model. Its scene strategy reads the same navigation entries that drive the app, then chooses a single-, two-, or three-pane presentation for the current window.
Confirm that list-detail matches the task
Use list-detail when the list remains useful after an item is open. It lets people compare peer items, move between them, or retain the collection as context for the detail.
| User task | Better fit |
|---|---|
| Browse peers and inspect one selected item | List-detail |
| Work on one primary item with related tools or properties | Supporting pane |
| Read immersive media, an article, or edit a large canvas | A focused single pane, even when wide |
| Choose a small value and return immediately | A dialog, sheet, or compact destination |
Do not add a second pane solely because an expanded window exists. A media viewer or form that needs sustained attention can be worse with competing content beside it. Adaptive Layouts for Phones, Tablets, and Foldables explains how to make that decision from available window space rather than a device label.
Use the current Material 3 Adaptive integration
Material 3 Adaptive 1.3.0 introduced the adaptive-navigation3 artifact and its Navigation 3 scene strategies. ListDetailSceneStrategy is currently marked @ExperimentalMaterial3AdaptiveApi, so localize the opt-in to the navigation host and check release notes when upgrading.
At the time of writing, the relevant stable artifacts are Navigation 3 1.1.6 and Material 3 Adaptive 1.3.0:
dependencies {
implementation("androidx.navigation3:navigation3-runtime:1.1.6")
implementation("androidx.navigation3:navigation3-ui:1.1.6")
implementation("androidx.compose.material3.adaptive:adaptive-navigation3:1.3.0")
}Use the versions managed by your dependency policy. The Navigation 3 release notes and Material 3 Adaptive release notes are the source of truth for newer versions.
This approach requires Navigation 3. If an app has not migrated yet, first model and test its back stack; do not bolt a manually managed right pane onto navigation that cannot represent the selected detail destination.
Model list and detail as navigation entries
The list is an entry in the back stack. Opening an item adds a detail entry. ListDetailSceneStrategy uses metadata to identify each role, and NavDisplay renders the appropriate scene.
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
import androidx.compose.runtime.Composable
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import kotlinx.serialization.Serializable
@Serializable
private data object ProjectList : NavKey
@Serializable
private data class ProjectDetail(
val projectId: String,
) : NavKey
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ProjectNavigation() {
val backStack = rememberNavBackStack<NavKey>(ProjectList)
val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>()
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
sceneStrategies = listOf(listDetailStrategy),
entryProvider = entryProvider {
entry<ProjectList>(
metadata = ListDetailSceneStrategy.listPane(
detailPlaceholder = { EmptyProjectSelection() },
),
) {
ProjectListScreen(
onOpenProject = { projectId ->
backStack.add(ProjectDetail(projectId))
},
)
}
entry<ProjectDetail>(
metadata = ListDetailSceneStrategy.detailPane(),
) { detail ->
ProjectDetailScreen(
projectId = detail.projectId,
onBack = { backStack.removeLastOrNull() },
)
}
},
)
}
@Composable
private fun EmptyProjectSelection() {
Text("Choose a project to view its details")
}This focused example follows the official Material list-detail recipe. Provide your own list and detail screen plus normal loading, error, state, and accessibility handling.
The significant part is where selection lives: ProjectDetail(projectId) is the selected destination on the back stack, not a second mutable selectedProject that must stay in sync with navigation. Deep links, restoration, and Back therefore describe the same destination whether it fills the window or shares it with the list.
How the strategy adapts
rememberListDetailSceneStrategy() supplies a Material adaptive strategy to NavDisplay. The strategy recognizes metadata roles, the current back stack, window size, and device state, then renders an appropriate scene.
| Situation | Back stack | Typical presentation |
|---|---|---|
| No item selected | ProjectList | The list; a wider detail region can show detailPlaceholder. |
| Compact window, item selected | ProjectList, ProjectDetail(id) | The selected detail as the current single destination. |
| Wider window, item selected | ProjectList, ProjectDetail(id) | List and detail shown together. |
| Very wide, genuine third relationship | List, detail, and extra entry | Up to list, detail, and extra panes together. |
The API also supports ListDetailSceneStrategy.extraPane() for a third destination, such as a related profile or inspector. Add it only when it has a durable product relationship. The strategy can show multiple navigation entries at once while the back stack remains the source of truth.
By default, shouldHandleSinglePaneLayout is false. This lets Navigation 3’s normal single-pane handling render compact layouts while the Material strategy takes over when it can build a multi-pane scene. Change it only after understanding your scene-strategy order; handling every compact case can change how overlays or custom scenes are selected.
Make pane roles and chrome honest
The metadata is architectural information:
listPane()identifies the collection that supplies context. Its optionaldetailPlaceholdermakes the empty-selection state intentional on wider windows.detailPane()identifies the selected item destination.extraPane()identifies a third related destination, not an arbitrary control area.
Keep the same sceneKey for roles in one list-detail relationship. The default Unit works for one relationship; use a distinct key only when a single NavDisplay hosts several independent list-detail groups.
Review app-bar and Back affordances in both presentations. A compact detail-only screen may need Up, but a permanently visible list changes that relationship. Do not leave a detail-level back arrow visible just because it existed in compact mode; it can duplicate system Back or imply that the visible list will disappear. A full-screen detail presentation should likewise be deactivated when that destination participates in a visible list-detail scene.
Let the window change the scene, not your domain state
The strategy calculates its default directive from currentWindowAdaptiveInfoV2(). It can react to split-screen resizing, rotation, and foldable posture changes without your destinations duplicating window checks.
Keep your state independent from the arrangement:
- Keep the selected ID in the detail navigation key.
- Keep data, loading, and unsaved edits in the appropriate destination state holder or
ViewModel. - Do not save an
isTwoPanepreference; it is environment state. - Do not recreate the list destination when switching between compact and wide scenes.
Window Size Classes in Jetpack Compose explains the window-level information behind this behavior. The scene strategy turns that information into pane behavior without every destination repeating the same conditions.
Why a manual Row becomes fragile
A Row with a list on the left and a detail on the right can suit a static dashboard, but it becomes fragile when that relationship is navigation:
- Compact screens need a defined detail route and Back behavior.
- Restoration and deep links must recreate the same selected destination.
- App bars, focus, loading states, and system insets need an owner in both arrangements.
- An unavailable right pane must not silently discard selection.
Navigation 3 scenes solve those concerns around navigation entries rather than a particular width. Prefer ListDetailSceneStrategy for an actual list-to-detail flow; use an adaptive grid or component reflow when the content has no navigable detail relationship.
Test the task, then the transition
Test more than a screenshot of two visible panes:
- In a compact window, select an item, scroll the list, and make an edit in detail if editing is supported.
- Resize, rotate, or fold/unfold into a wider layout.
- Verify list and the same detail destination appear together without losing selection, data, draft state, or focus.
- Test system Back, app-bar Up where it exists, a deep link to a detail item, and empty selection.
- Repeat with large font scaling, visible system bars, keyboard navigation, and a very wide window if an extra pane exists.
Preview and screenshot test compact, medium, and expanded layouts, then test the live transition. Edge-to-Edge UI in Jetpack Compose is a useful companion: panes and dividers still need reachable controls around system bars, cutouts, and the IME.
Common mistakes
Treating a detail pane as copied list state
The selected detail is a navigation destination. Store its key in the back stack and let the scene decide whether it is full-screen or simultaneously visible.
Starting with a third pane
Three panes add focus, sizing, and Back complexity. First make list and detail coherent; add extraPane() only for a task-specific third destination.
Hard-coding a device check
Do not use isTablet or landscape alone to decide whether panes show. A window can resize at runtime. Let the adaptive directive and scene strategy respond to current window information.
Leaving a blank detail region
A wide list with no selection should not display an unexplained empty panel. Provide detailPlaceholder that tells people what to do next.
FAQ
Can I use this without Navigation 3?
Not this scene-strategy integration. It is designed for NavDisplay and Navigation 3 NavEntrys. Apps without Navigation 3 can use other Material adaptive APIs, but should not imitate it with an isolated second-pane state machine.
Does it always show two panes on a tablet?
No. It adapts to current window size and device state. A tablet in split-screen can be too narrow, and a compact phone uses the same list and detail destinations sequentially.
When should I use a supporting pane instead?
Use it when one destination remains the primary task and a second destination complements it with tools, properties, or context. Use list-detail when the user’s main action is selecting a peer from a collection and inspecting that selected item.