How to make your Compose layouts adapt for phone, tablet, and foldables

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

Post a Comment

Previous Post Next Post