Build Responsive List + Detail Screens in Jetpack Compose
Overview
Creating a UI that feels right on every device can be tricky, but the ListDetailPaneScaffold
from the Material 3 adaptive-layout library makes it a breeze.
Whether it’s a chat app, email client, or note-taking tool, this component adjusts effortlessly to:
- Phones: Shows a list, then slides into a detail screen when you tap an item.
- Tablets: Puts the list and detail side-by-side for a smooth, connected feel.
- Foldables: Adapts instantly as the screen size or orientation shifts.
Adaptive Layout: A clever UI that reshapes itself to fit the device’s screen, making sure your users always get a great experience.
This guide will walk you through building a polished, responsive list-detail UI that aligns with Material Design 3, perfect for today’s Android apps.
When to Use ListDetailPaneScaffold
The ListDetailPaneScaffold
is your go-to when you need a clean, device-friendly layout.
Here’s when it works best:
- Master-Detail Flow: Think email apps (list of emails, email content), notes (titles, full note), or music apps (playlists, song details).
- Cross-Device Support: Ideal for apps that need to shine on phones, tablets, and foldables without custom layout headaches.
- Simple Navigation: Automatically manages list-to-detail transitions and backstack, so you don’t have to sweat the small stuff.
Heads-Up:
Pass on this if your app is a single-screen deal or needs a highly customized layout, as it’s built for standard list-detail patterns.
Properties and Usage Table
Property | Usage |
---|---|
ListDetailPaneScaffold |
The core component that handles adaptive list and detail panes, switching between single and dual-pane layouts based on the device. |
rememberListDetailPaneScaffoldNavigator |
Tracks navigation state, ensuring smooth transitions between list and detail views with proper backstack support. |
directive and value |
Guides the layout behavior and keeps track of the current state, deciding whether to show the list, detail, or both. |
listPane |
Holds the list UI, typically using LazyColumn for efficient, smooth scrolling of items. |
detailPane |
Shows the selected item’s details, with built-in support for navigating back to the list. |
BackHandler |
Manages the system back button to return from the detail view to the list view. |
Implementation Steps
Let’s jump into building a responsive list-detail UI with ListDetailPaneScaffold
.
Step 1: Add Dependencies
Start by adding the Material 3 adaptive-layout dependencies to your build.gradle
file.
// Material 3 adaptive layout dependencies
implementation "androidx.compose.material3.adaptive:adaptive:1.2.0-alpha11"
implementation "androidx.compose.material3.adaptive:adaptive-layout:1.2.0-alpha11"
implementation "androidx.compose.material3.adaptive:adaptive-navigation:1.2.0-alpha11"
// Optional: Extended icons for a polished UI
implementation "androidx.compose.material:material-icons-extended"
Step 2: Set Up Main Activity
Create the main activity to launch the Jetpack Compose UI.
package com.android.jetpackcomposepractice
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
// MainActivity: Entry point for the app, setting up the Compose UI
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
ListDetailScreen()
}
}
}
}
}
Step 3: Create List-Detail Scaffold
Implement ListDetailPaneScaffold
to manage adaptive layouts and navigation seamlessly.
@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun ListDetailScreen() {
// Initialize navigator for managing navigation state
val navigator = rememberListDetailPaneScaffoldNavigator()
// Coroutine scope for handling asynchronous navigation
val coroutineScope = rememberCoroutineScope()
// Handle system back button navigation
BackHandler(enabled = navigator.canNavigateBack()) {
coroutineScope.launch {
navigator.navigateBack()
}
}
// Main scaffold for adaptive list-detail layout
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
// List pane: Displays clickable items in a lazy column
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
) {
items((1..20).map { "Item $it" }) { item ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp)
.clickable {
coroutineScope.launch {
navigator.navigateTo(
ListDetailPaneScaffoldRole.Detail,
item
)
}
},
elevation = CardDefaults.cardElevation(4.dp),
shape = MaterialTheme.shapes.medium
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = item,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
},
detailPane = {
// Detail pane: Shows selected item details or placeholder
val item = navigator.currentDestination?.contentKey
if (item != null) {
Column(modifier = Modifier.fillMaxSize()) {
// TopAppBar for navigation in detail pane
TopAppBar(
title = { Text(text = "Details") },
navigationIcon = {
IconButton(onClick = {
coroutineScope.launch { navigator.navigateBack() }
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
}
)
// Card displaying item details
Card(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
elevation = CardDefaults.cardElevation(6.dp),
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = item,
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Here are more details about $item. " +
"This could include description, metadata, or actions.",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
} else {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Select an item from the list")
}
}
}
)
}
Full Source Code
Here’s the complete source code, ready to drop into your project for a fully functional list-detail UI.
MainActivity.kt
package com.android.jetpackcomposepractice
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
ListDetailScreen()
}
}
}
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun ListDetailScreen() {
val navigator = rememberListDetailPaneScaffoldNavigator<String>()
val coroutineScope = rememberCoroutineScope()
// ✅ Handle system back button
BackHandler(enabled = navigator.canNavigateBack()) {
coroutineScope.launch {
navigator.navigateBack()
}
}
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
) {
items((1..20).map { "Item $it" }) { item ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp)
.clickable {
coroutineScope.launch {
navigator.navigateTo(
ListDetailPaneScaffoldRole.Detail,
item
)
}
},
elevation = CardDefaults.cardElevation(4.dp),
shape = MaterialTheme.shapes.medium
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = item,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
},
detailPane = {
val item = navigator.currentDestination?.contentKey
if (item != null) {
Column(modifier = Modifier.fillMaxSize()) {
// ✅ TopAppBar for detail pane
TopAppBar(
title = { Text(text = "Details") },
navigationIcon = {
IconButton(onClick = {
coroutineScope.launch { navigator.navigateBack() }
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
}
)
// ✅ Card in detail view
Card(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
elevation = CardDefaults.cardElevation(6.dp),
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = item,
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Here are more details about $item. " +
"This could include description, metadata, or actions.",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
} else {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Select an item from the list")
}
}
}
)
}
Conclusion
Creating a UI that feels at home on any device be it a phone, tablet, or foldable can feel daunting, but ListDetailPaneScaffold
makes it surprisingly straightforward.
It’s like having a trusty guide that takes care of layout switches and navigation, letting you focus on building a great app.
Whether you’re crafting an email client, a music player, or a note-taking app, this component ensures your app looks sharp and works intuitively across all devices.
With Material Design 3 as its backbone, your UI stays sleek and modern.