← All blogs
Android

Adaptive Android Apps with Jetpack Compose: Build Once for Every Screen

Anand Gaur
Mobile Tech Lead · 13 Aug 2026
Adaptive Android Apps with Jetpack Compose: Build Once for Every Screen

Have you ever opened an app on a tablet and felt like you were looking at a stretched phone app? A huge blank space on both sides, one giant list in the middle, and buttons that look lost on the big screen. That is exactly the problem adaptive apps solve.

In this blog, we will go from absolute basics to advanced concepts of building adaptive Android apps with Jetpack Compose. By the end, you will understand what adaptive apps are, why Google pushed this idea so hard, what problems existed before, how to set everything up, and how to build real-world adaptive layouts step by step. My goal is simple: after reading this one blog, you should not need to read any other blog on this topic.

Let us start from the very beginning.


The World Before Adaptive Apps: What Was the Problem?

To understand why adaptive apps exist, we need to go back in time a little.

1. Android was born for one screen size

When Android started in 2008, almost every device was a small phone. Developers designed one layout, tested it on one or two phones, and shipped the app. Life was simple.

Then the ecosystem exploded. Today Android runs on:

  • Small phones and big phones

  • Foldables (like Samsung Galaxy Fold and Pixel Fold) that change size while the app is running

  • Tablets of many sizes

  • Chromebooks where apps run in resizable windows

  • Desktop mode where a phone connects to a monitor

  • Cars, TVs, and even XR headsets

There are more than 3 billion active Android devices, and they come in thousands of screen shapes and sizes. One fixed layout simply cannot work for all of them.

2. The old solutions were painful

Before Compose and adaptive APIs, developers tried to handle this in the XML world using these tricks:

a) Multiple layout folders

We used to create separate XML files for different screens:

res/layout/activity_main.xml          -> phones
res/layout-sw600dp/activity_main.xml  -> 7 inch tablets
res/layout-sw720dp/activity_main.xml  -> 10 inch tablets
res/layout-land/activity_main.xml     -> landscape

The problem? You are now maintaining 3 or 4 copies of the same screen. Change one button, and you must change it in every file. Miss one file and you get a bug that only appears on tablets. Testing became a nightmare.

b) The sw600dp assumption broke down

The whole system assumed a simple rule: “small screen means phone, big screen means tablet.” Foldables destroyed this assumption. A foldable is a phone when closed and a tablet when open, and it switches between the two while your app is running. Chromebooks made it worse because the user can resize your app window with a mouse, just like on a desktop, so your app can go from tiny to huge in one drag.

c) Fragments with dual-pane logic

Google’s old advice was to use Fragments: show one Fragment on phones, and two Fragments side by side on tablets. It worked, but the code was complex. You had to manage two different navigation flows, handle back stack differently on each device type, and keep both flows in sync. Many teams simply gave up and shipped the stretched phone UI on tablets.

d) Checking the device instead of the window

A very common old mistake was code like this:

val isTablet = resources.configuration.smallestScreenWidthDp >= 600

This checks the device. But on a foldable or a Chromebook, the device size does not tell you the window size. Your app might be running in split-screen mode on a huge tablet, getting only half the screen. Device checks give the wrong answer in all these cases.

3. Why Google suddenly started caring so much

A few things happened together:

  • Foldables became mainstream and their sales kept growing every year

  • Large screen Android devices (tablets, foldables, Chromebooks) crossed hundreds of millions of active users

  • Google launched the Pixel Tablet and Pixel Fold, so they needed the ecosystem to look good on them

  • Play Store started ranking and reviewing apps based on large screen quality, and it shows warnings for apps that are not optimized

  • Android 16 introduced an even bigger change: on large screens, the system ignores fixed orientation and resizability restrictions. Apps can no longer say “I only run in portrait.” Your app will be resized whether you like it or not

So the message from Google became loud and clear: your app does not run on a device, it runs in a window, and that window can be any size. Design for the window.

This is exactly where adaptive design and Jetpack Compose come in.


What Exactly is an Adaptive App?

Let us define it in one line:

An adaptive app changes its layout based on the space available to it, so that it always makes the best use of the screen.

Notice the words carefully: “space available to it,” not “device it runs on.”

A simple example makes it clear. Think of Gmail:

  • On a phone, you see a list of emails. Tap one, and it opens a new screen with the email content.

  • On a tablet, you see the list on the left and the opened email on the right, both at the same time.

Same app, same feature, but the layout adapts. That is adaptive design.

Responsive vs Adaptive: are they the same?

People use both words loosely, but there is a small difference:

  • Responsive means the UI stretches and shrinks smoothly. A grid showing 2 columns becomes 4 columns on a wider screen.

  • Adaptive means the UI structure itself changes. A bottom navigation bar becomes a navigation rail. A single-pane screen becomes a two-pane screen.

In practice, a good adaptive app does both. Compose gives us tools for both, and we will cover all of them.

Why Jetpack Compose is perfect for this

In the XML world, adapting meant maintaining multiple layout files. In Compose, your UI is just Kotlin code. An if condition can decide the entire structure of your screen:

if (windowIsWide) {
    TwoPaneLayout()
} else {
    SinglePaneLayout()
}

One codebase, one screen, one source of truth. This is why the title of this blog says “Build Once for Every Screen.” Compose makes adaptive design a normal programming problem instead of a resource-folder juggling act.


The Foundation: Window Size Classes

Now we start with the most important concept in the whole adaptive world: Window Size Classes.

The idea

You cannot write separate code for every possible width like 320dp, 411dp, 600dp, 840dp, and so on. So Google grouped all possible window sizes into a few named buckets called size classes.

For width, the buckets are:

Width Size Class Window Width Typical Example Compact less than 600dp Phone in portrait Medium 600dp to 839dp Foldable open, small tablet, phone in split screen Expanded 840dp to 1199dp Tablet in landscape, Chromebook window Large 1200dp to 1599dp Big tablet, desktop window Extra Large 1600dp and above Full desktop, external monitor

(Large and Extra Large were added recently in the newer versions of the library. For most apps, handling Compact, Medium, and Expanded covers almost everything.)

For height, there are three buckets: Compact, Medium, and Expanded. Height classes are useful for things like landscape phones, where the width is fine but the height is very small.

The golden rule

Always make layout decisions based on the window size class, never based on the device type, screen size, or orientation.

Why? Because:

  • A tablet in split-screen mode gives your app a Compact window

  • A foldable changes from Compact to Medium or Expanded when unfolded

  • A Chromebook user can drag your window to any size

  • Android 16 on large screens will resize your app freely

The window is the truth. The device is a lie.

About 90 percent of devices at these breakpoints

One more practical fact: the 600dp and 840dp numbers are not random. Google analyzed the entire device ecosystem and picked breakpoints so that most real devices fall clearly into one bucket. Phones in portrait are almost always under 600dp wide. Unfolded foldables usually land in Medium. Tablets in landscape land in Expanded. So these three buckets map beautifully to the real world.


Setting Up Your Project

Let us get our hands dirty. Here is everything you need.

Step 1: Add the dependencies

In your module level build.gradle.kts:

dependencies {
    // Core adaptive APIs: gives you currentWindowAdaptiveInfo()
    implementation(”androidx.compose.material3.adaptive:adaptive:1.1.0”)

// Adaptive layouts: ListDetailPaneScaffold, SupportingPaneScaffold
    implementation(”androidx.compose.material3.adaptive:adaptive-layout:1.1.0”)
    // Navigation helpers for the scaffolds above
    implementation(”androidx.compose.material3.adaptive:adaptive-navigation:1.1.0”)
    // NavigationSuiteScaffold: auto-switching navigation bar / rail
    implementation(”androidx.compose.material3:material3-adaptive-navigation-suite:1.3.2”)
}

A quick note on what lives where, because this confuses many developers:

  • material3.adaptive libraries give you window info and the pane scaffolds

  • material3-adaptive-navigation-suite gives you the smart navigation component

  • Under the hood, these use the androidx.window library, which reads the real window metrics from the system

Step 2: Make sure your Activity supports resizing

In AndroidManifest.xml, do not lock the orientation and do not disable resizing:

<activity
    android:name=”.MainActivity”
    android:exported=”true”>
    <!-- Do NOT add android:screenOrientation=”portrait” -->
    <!-- Do NOT add android:resizeableActivity=”false” -->
</activity>

If your app locks portrait today, remember that Android 16 ignores this on large screens anyway. It is better to remove the restriction yourself and handle all sizes properly.

Step 3: Handle configuration changes gracefully

When a foldable unfolds or a window resizes, the Activity may be recreated. Compose handles most of this well if you follow normal state practices:

  • Use rememberSaveable for UI state that must survive recreation

  • Keep business state in a ViewModel

  • Never store “isTablet” style flags anywhere

That is it. No special manifest flags are required for basic adaptive behavior.


Level 1 (Basic): Reading the Window Size Class

Here is the simplest possible adaptive code. This one function is the heart of everything:

import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.window.core.layout.WindowWidthSizeClass

@Composable
fun HomeScreen() {
    val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
    when (windowSizeClass.windowWidthSizeClass) {
        WindowWidthSizeClass.COMPACT -> {
            CompactHomeLayout()   // single column, bottom nav
        }
        WindowWidthSizeClass.MEDIUM -> {
            MediumHomeLayout()    // maybe 2 columns, nav rail
        }
        WindowWidthSizeClass.EXPANDED -> {
            ExpandedHomeLayout()  // two panes, nav rail or drawer
        }
    }
}

currentWindowAdaptiveInfo() is a composable function that gives you live information about the current window. The magic part: when the window size changes (fold, unfold, resize, split screen), Compose automatically recomposes and your when block picks the new branch. You write zero code for detecting the change.

A tiny but complete example

Let us build a profile card that shows image and text vertically on phones, and side by side on bigger windows:

@Composable
fun ProfileCard() {
    val widthClass = currentWindowAdaptiveInfo()
        .windowSizeClass.windowWidthSizeClass

if (widthClass == WindowWidthSizeClass.COMPACT) {
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            ProfileImage()
            ProfileDetails()
        }
    } else {
        Row(verticalAlignment = Alignment.CenterVertically) {
            ProfileImage()
            Spacer(Modifier.width(24.dp))
            ProfileDetails()
        }
    }
}

Congratulations, you just wrote your first adaptive UI. Everything else in this blog is a more powerful version of this same idea.


Level 2 (Intermediate): Adaptive Navigation with NavigationSuiteScaffold

Navigation is the first thing users notice on a big screen. Material Design guidance is:

  • Compact window: Bottom Navigation Bar (thumb friendly)

  • Medium window: Navigation Rail on the left (vertical strip of icons)

  • Expanded and larger: Navigation Rail, or a Permanent Navigation Drawer for apps with many destinations

Writing this switching logic by hand is boring. So Material 3 gives us NavigationSuiteScaffold, which does it automatically:

enum class AppDestination(val label: String, val icon: ImageVector) {
    HOME(”Home”, Icons.Default.Home),
    SEARCH(”Search”, Icons.Default.Search),
    PROFILE(”Profile”, Icons.Default.Person)
}

@Composable
fun App() {
    var currentDestination by rememberSaveable {
        mutableStateOf(AppDestination.HOME)
    }
    NavigationSuiteScaffold(
        navigationSuiteItems = {
            AppDestination.entries.forEach { destination ->
                item(
                    selected = destination == currentDestination,
                    onClick = { currentDestination = destination },
                    icon = { Icon(destination.icon, contentDescription = destination.label) },
                    label = { Text(destination.label) }
                )
            }
        }
    ) {
        // Screen content for the selected destination
        when (currentDestination) {
            AppDestination.HOME -> HomeScreen()
            AppDestination.SEARCH -> SearchScreen()
            AppDestination.PROFILE -> ProfileScreen()
        }
    }
}

Run this on a phone: you get a bottom bar. Run it on a tablet or unfold a foldable: the bottom bar becomes a navigation rail on the left. You wrote the navigation once, and it adapts everywhere. This single component removes a huge amount of boilerplate.

If you ever need custom control, you can override the decision:

val adaptiveInfo = currentWindowAdaptiveInfo()
val layoutType = if (
    adaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED
) {
    NavigationSuiteType.NavigationDrawer
} else {
    NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(adaptiveInfo)
}

NavigationSuiteScaffold(
    layoutType = layoutType,
    navigationSuiteItems = { /* ... */ }
) { /* content */ }

Level 3 (Intermediate Plus): Canonical Layouts

Google studied hundreds of well-designed large screen apps and found that most good adaptive screens follow one of three patterns. These are called canonical layouts. Learn these three and you can design 90 percent of adaptive screens.

1. List-Detail

The most common pattern. A list of items on the left, and details of the selected item on the right.

  • Compact: show only the list, tapping an item navigates to a detail screen

  • Expanded: show both panes side by side

Real examples: Gmail (email list + open email), WhatsApp on tablets (chat list + conversation), any news app (headlines + article).

2. Feed

A grid of content cards that reflows based on width. One column on phones, two or three columns on tablets.

Real examples: Instagram explore page, Google News, Play Store home.

3. Supporting Pane

A main content area plus a secondary pane with helper content. The main content stays primary on all sizes; the supporting pane appears on the side only when space allows.

Real examples: a video player with a related-videos pane, a document editor with a comments pane, a music player with a queue pane.

Compose Material 3 Adaptive gives ready-made scaffolds for List-Detail and Supporting Pane. Let us build the big one properly.


Level 4 (Advanced): ListDetailPaneScaffold, the Real Deal

This is the component that makes “build once for every screen” truly real. Let us build a complete contacts app screen with it.

The full code

import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold

data class Contact(val id: Int, val name: String, val phone: String)
val contacts = List(30) { Contact(it, “Contact $it”, “+91 98765 432$it”) }
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ContactsScreen() {
    // The navigator understands the window size and manages
    // which panes are visible and the back stack between them
    val navigator = rememberListDetailPaneScaffoldNavigator<Int>()
    val scope = rememberCoroutineScope()
    NavigableListDetailPaneScaffold(
        navigator = navigator,
        listPane = {
            AnimatedPane {
                ContactList(
                    contacts = contacts,
                    onContactClick = { contactId ->
                        scope.launch {
                            navigator.navigateTo(
                                pane = ListDetailPaneScaffoldRole.Detail,
                                contentKey = contactId
                            )
                        }
                    }
                )
            }
        },
        detailPane = {
            AnimatedPane {
                val contactId = navigator.currentDestination?.contentKey
                if (contactId != null) {
                    ContactDetail(contact = contacts[contactId])
                } else {
                    // Placeholder when nothing is selected (visible on large screens)
                    EmptyDetailPlaceholder()
                }
            }
        }
    )
}
@Composable
fun ContactList(contacts: List<Contact>, onContactClick: (Int) -> Unit) {
    LazyColumn {
        items(contacts) { contact ->
            ListItem(
                headlineContent = { Text(contact.name) },
                supportingContent = { Text(contact.phone) },
                modifier = Modifier.clickable { onContactClick(contact.id) }
            )
        }
    }
}
@Composable
fun ContactDetail(contact: Contact) {
    Column(Modifier.padding(24.dp)) {
        Text(contact.name, style = MaterialTheme.typography.headlineMedium)
        Spacer(Modifier.height(8.dp))
        Text(contact.phone, style = MaterialTheme.typography.bodyLarge)
    }
}

What this gives you for free

Read this list slowly, because every point here used to be days of manual work:

  1. On a phone (Compact): only the list is visible. Tapping a contact slides in the detail as a full screen. Pressing back returns to the list. The scaffold manages this navigation itself.

  2. On a tablet (Expanded): list and detail appear side by side. Tapping a contact just updates the right pane. No navigation happens because none is needed.

  3. On a foldable: fold and unfold the device, and the UI smoothly moves between the two behaviors. Selection state is preserved.

  4. Back handling: the NavigableListDetailPaneScaffold wires up back navigation correctly for each mode automatically.

  5. Animations: AnimatedPane gives you polished pane transition animations for free.

This is the true meaning of adaptive: not two different screens, but one screen that knows how to arrange itself.

SupportingPaneScaffold

The supporting pane pattern works almost the same way:

val navigator = rememberSupportingPaneScaffoldNavigator()

SupportingPaneScaffold(
    directive = navigator.scaffoldDirective,
    value = navigator.scaffoldValue,
    mainPane = { AnimatedPane { VideoPlayer() } },
    supportingPane = { AnimatedPane { RelatedVideosList() } }
)

On large windows, the related videos show beside the player. On compact windows, only the player shows, and you can reveal the supporting pane through navigation.


Level 5 (Advanced): Foldables and Postures

Foldables add one more dimension: the fold itself. The WindowAdaptiveInfo object also exposes window posture information.

The two special postures

  1. Tabletop posture: the device is half folded and kept on a table like a mini laptop. Think of watching a video with the top half showing the video and the bottom half showing controls.

  2. Book posture: the device is half folded and held vertically like a book, with a vertical hinge in the middle.

Reading posture in Compose

@Composable
fun VideoScreen() {
    val windowAdaptiveInfo = currentWindowAdaptiveInfo()
    val posture = windowAdaptiveInfo.windowPosture
if (posture.isTabletop) {
        // Split the UI around the horizontal fold
        Column {
            VideoPlayer(Modifier.weight(1f))   // top half
            PlayerControls(Modifier.weight(1f)) // bottom half
        }
    } else {
        // Normal full screen video with overlay controls
        FullScreenVideoPlayer()
    }
}

The windowPosture also gives you hingeList, a list of hinge positions and properties, so advanced apps can avoid placing important content exactly on the physical fold line. The pane scaffolds we saw earlier already respect the hinge automatically: on a book-posture foldable, ListDetailPaneScaffold splits the panes exactly at the hinge. Again, free behavior.

Real world uses of postures:

  • YouTube uses tabletop mode for video on top, comments below

  • Camera apps show the viewfinder on top and controls at the bottom

  • Video calling apps put the other person on top and your controls below


Level 6 (Advanced): Fine-Grained Adaptation Inside a Screen

Window size classes decide the big structure. But inside a pane, you often need smaller, local decisions. For that, do not use the window size class. Use the actual space available to that specific composable.

Why not window size class everywhere?

Imagine a card that lives inside the detail pane of a ListDetailPaneScaffold. On an Expanded window the pane itself might be narrow, because the list pane took half the space. If the card checks the window class, it thinks “Expanded, lots of space” and renders a wide layout inside a narrow pane. Broken UI.

The rule: window size class for screen-level decisions, local measurement for component-level decisions.

Option 1: BoxWithConstraints

@Composable
fun StatsCard() {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column { StatItem1(); StatItem2(); StatItem3() }
        } else {
            Row { StatItem1(); StatItem2(); StatItem3() }
        }
    }
}

BoxWithConstraints measures the space given to this exact composable and lets you branch on it. Perfect for reusable components that can be placed anywhere.

Option 2: Adaptive lazy grids

For feed style content, let the grid do the math:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 160.dp)
) {
    items(products) { product ->
        ProductCard(product)
    }
}

GridCells.Adaptive(160.dp) means: fit as many 160dp columns as possible. Phone gets 2 columns, small tablet gets 4, big monitor gets 7. Zero conditions written by you. This one line is the entire “Feed” canonical layout.

Option 3: FlowRow and FlowLayout

For chips, tags, and filters that should wrap to the next line when space ends:

FlowRow {
    tags.forEach { tag ->
        FilterChip(selected = false, onClick = {}, label = { Text(tag) })
    }
}

Real-World Example: Building “NewsHub”, a Complete Adaptive News App

Let us connect everything into one realistic app structure so you can see how the pieces fit together. NewsHub has three destinations: Feed, Bookmarks, and Profile. The Feed opens articles in a list-detail pattern.

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun NewsHubApp() {
    var currentDestination by rememberSaveable { mutableStateOf(NewsDestination.FEED) }

    // Layer 1: Adaptive navigation shell
    NavigationSuiteScaffold(
        navigationSuiteItems = {
            NewsDestination.entries.forEach { dest ->
                item(
                    selected = dest == currentDestination,
                    onClick = { currentDestination = dest },
                    icon = { Icon(dest.icon, contentDescription = dest.label) },
                    label = { Text(dest.label) }
                )
            }
        }
    ) {
        when (currentDestination) {
            NewsDestination.FEED -> NewsFeedScreen()       // Layer 2 below
            NewsDestination.BOOKMARKS -> BookmarksScreen()
            NewsDestination.PROFILE -> ProfileScreen()
        }
    }
}

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun NewsFeedScreen(viewModel: NewsViewModel = viewModel()) {
    val articles by viewModel.articles.collectAsStateWithLifecycle()
    val navigator = rememberListDetailPaneScaffoldNavigator<String>()
    val scope = rememberCoroutineScope()

    // Layer 2: List-Detail canonical layout
    NavigableListDetailPaneScaffold(
        navigator = navigator,
        listPane = {
            AnimatedPane {
                // Layer 3: Adaptive grid inside the pane
                LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 300.dp)) {
                    items(articles, key = { it.id }) { article ->
                        ArticleCard(
                            article = article,
                            onClick = {
                                scope.launch {
                                    navigator.navigateTo(
                                        ListDetailPaneScaffoldRole.Detail,
                                        article.id
                                    )
                                }
                            }
                        )
                    }
                }
            }
        },
        detailPane = {
            AnimatedPane {
                val articleId = navigator.currentDestination?.contentKey
                articleId?.let { ArticleReader(articleId = it) }
                    ?: SelectArticlePlaceholder()
            }
        }
    )
}

Now trace what a user experiences:

  • Phone: bottom navigation bar, single column feed, tapping an article opens it full screen, back button returns to the feed.

  • Foldable unfolded: navigation rail appears on the left, feed becomes two columns, tapping an article shows it beside the feed, and the split respects the hinge.

  • Tablet landscape: navigation rail, wide two-pane reading experience like a real news reader.

  • Chromebook, resizing the window: the app fluidly moves between all these states as the user drags the window edge.

Three layers of adaptation, and each layer is independent and simple. This layered thinking is the professional way to structure adaptive apps:

  1. Shell layer: NavigationSuiteScaffold decides navigation UI

  2. Screen layer: pane scaffolds decide single vs multi pane

  3. Component layer: adaptive grids, BoxWithConstraints, FlowRow decide local details


State Management in Adaptive Apps: The Hidden Difficulty

This part is skipped by most tutorials, but it is where real apps break. When your app can be resized any moment, state handling needs care.

Rule 1: State must survive recreation

Fold and unfold events can recreate your Activity. So:

// Bad: lost on recreation
var selectedId by remember { mutableStateOf<String?>(null) }

// Good: survives recreation
var selectedId by rememberSaveable { mutableStateOf<String?>(null) }
// Best for business data: ViewModel + StateFlow
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

Rule 2: Hoist selection state above the layout decision

The selected item must not live inside the phone layout or the tablet layout. It must live above both, so that when the layout switches, the selection is preserved. The ListDetailPaneScaffold navigator does this correctly by design, which is one more reason to prefer it over hand-rolled solutions.

Rule 3: Think about what a size change should mean

If the user is reading article 5 on a phone and then unfolds the device, they should see the feed on the left with article 5 still open on the right. Not a reset. Not the placeholder. Walk through these transitions mentally for every screen you build.


Testing and Previewing Adaptive UIs

You cannot buy every device, and you do not need to. Here is the practical testing toolkit.

1. Multipreview in Compose

@Preview(name = “Phone”, device = “spec:width=411dp,height=891dp”)
@Preview(name = “Foldable”, device = “spec:width=673dp,height=841dp”)
@Preview(name = “Tablet”, device = “spec:width=1280dp,height=800dp”)
@Composable
fun NewsFeedPreview() {
    NewsHubTheme { NewsFeedScreen() }
}

Or even simpler, use the built-in annotation that generates all of these at once:

@PreviewScreenSizes
@Composable
fun NewsFeedAllSizes() {
    NewsHubTheme { NewsFeedScreen() }
}

2. The Resizable Emulator

Android Studio ships a “Resizable (Experimental)” emulator device. It lets you switch between phone, foldable, and tablet modes at runtime with one click. This is the single best tool for adaptive testing because you can watch the transitions happen live.

3. Desktop mode and split screen

Test your app in split-screen mode on a tablet, and in freeform window mode. These expose the “device is big but my window is small” cases that break naive apps.

4. Automated screenshot tests

For teams, run screenshot tests at Compact, Medium, and Expanded widths. Any accidental layout break shows up as an image diff in the pull request instead of a one-star review from a tablet user.


Best Practices and Common Mistakes

Let me compress years of large screen migration lessons into a short checklist.

Do these:

  1. Decide layouts from window size classes, not device properties

  2. Use NavigationSuiteScaffold for navigation and pane scaffolds for content

  3. Use GridCells.Adaptive for any grid content

  4. Keep content width readable on huge screens. Text lines longer than about 60 to 75 characters are hard to read, so cap content width or add padding on Expanded and above

  5. Support keyboard, mouse, and stylus. Large screen users expect them. Compose handles most of it, but verify focus and hover behavior

  6. Hoist state, use rememberSaveable and ViewModels

  7. Test with the resizable emulator on every feature

Avoid these:

  1. android:screenOrientation="portrait" in the manifest. Android 16 ignores it on large screens, and Play flags it

  2. if (isTablet) checks using screen width of the device

  3. Stretching a phone layout and calling it a tablet layout. Empty space is wasted opportunity

  4. Making layout decisions from orientation. A landscape phone and a portrait tablet can have the same width class for opposite reasons

  5. Putting important buttons exactly on the fold line of a foldable

  6. Forgetting the detail pane placeholder state on large screens. An empty right pane with no message looks broken


Summary: The Whole Blog in 10 Lines

  1. Android now runs on phones, foldables, tablets, Chromebooks, and desktops, so one fixed layout fails

  2. Old XML approaches needed duplicate layouts and fragile device checks

  3. Adaptive design means responding to the window size, not the device type

  4. Window Size Classes (Compact, Medium, Expanded, and newer Large, Extra Large) are the foundation

  5. currentWindowAdaptiveInfo() gives you the size class, and Compose recomposes automatically on changes

  6. NavigationSuiteScaffold switches between bottom bar, rail, and drawer for you

  7. Canonical layouts (List-Detail, Feed, Supporting Pane) solve most screens, with ready scaffolds in Material 3 Adaptive

  8. Postures and hinge info let you build special foldable experiences like tabletop mode

  9. Use BoxWithConstraints, adaptive grids, and FlowRow for local, component-level adaptation

  10. Hoist your state, remove orientation locks, and test with the resizable emulator


Final Thoughts

Adaptive design in Android is not an optional polish anymore. With foldables selling in millions, Play Store rewarding large screen quality, and Android 16 taking away orientation locks on big screens, adaptive is simply how Android apps are built now.

The good news is that Jetpack Compose has made this easier than it has ever been. What used to be duplicate layout files, fragment gymnastics, and fragile device checks is now a couple of scaffolds and a handful of when branches. You genuinely build once, and it runs beautifully everywhere.

Start small. Pick one screen in your current app, replace its navigation with NavigationSuiteScaffold, convert its main flow to ListDetailPaneScaffold, and run it on the resizable emulator. Watching your own app transform between a phone and a tablet with the same code is the moment adaptive design clicks forever.

Happy coding, and see you in the next one.


If you found this helpful, share it with a fellow Android developer who is still shipping stretched phone UIs on tablets. They will thank you.

Level Up Your Mobile Developer Interview !

Mastering AI for Android Developers

Your complete hands-on guide to integrating AI into Android apps — covering Generative AI, LLMs, on-device intelligence, AI APIs, real-world use cases, and practical implementation with modern Android development.
👉 Grab your copy now:
https://medium.com/@anandgaur2207/mastering-ai-for-android-developers-5cc6d62e7d21

Cracking the Mobile System Design Interview Book

Your complete practical guide to mastering Mobile System Design Interviews — covering scalable architecture, Android & iOS system design concepts, high-level design strategies, low-level design patterns, performance optimization, offline-first architecture, real-world case.
👉 Grab your copy now:
https://medium.com/@anandgaur2207/cracking-the-mobile-system-design-interview-book-8ff043db0359

Data Structures & Algorithms for Mobile App Developers Book

Master the Data Structures & Algorithms concepts every Android, iOS, Flutter, React Native, and KMP developer should know. Learn arrays, linked lists, trees, graphs, dynamic programming, searching, sorting, recursion, and problem-solving techniques with practical coding examples and interview-focused explanations.

👉 Grab your copy now:
https://medium.com/@anandgaur2207/data-structures-algorithms-for-mobile-app-developers-74db0ae17376?sharedUserId=anandgaur2207

Crack Android Interviews Like a Pro

Your complete Android interview preparation book — packed with real questions, deep explanations, and practical insights to help you stand out.
👉 Grab your copy now:
https://medium.com/@anandgaur2207/crack-android-interviews-with-confidence-the-only-handbook-youll-need-b87ec525f19c

iOS Developer Interview Handbook

From Swift fundamentals to advanced iOS concepts — a complete handbook to help you prepare smartly and confidently.
👉 Explore the book:
https://medium.com/@anandgaur2207/crack-ios-developer-interviews-with-confidence-the-complete-ios-developer-handbook-f1eabc3d7a21

Flutter Developer Interview Handbook

Ace your next Flutter interview with scenario-based questions, detailed explanations, and hands-on examples that make you stand out.
👉 Explore the book:
https://medium.com/@anandgaur2207/crack-flutter-developer-interviews-with-confidence-the-complete-flutter-developer-interview-6cb53996832c

React Native Developer Interview Handbook

Crack your next React Native interview with confidence!
This guide is packed with scenario-based questions, detailed explanations, and hands-on examples to help you stand out and succeed.
👉 Explore the book:
https://medium.com/@anandgaur2207/react-native-interview-crack-your-next-interview-with-confidence-0d7255a20fe1

Need 1:1 Career Guidance or Mentorship?

If you’re looking for personalized guidance, interview preparation help, or just want to talk about your career path in mobile development — you can book a 1:1 session with me on Topmate.

🔗 Book a session here

I’ve helped many developers grow in their careers, switch jobs, and gain clarity with focused mentorship. Looking forward to helping you too!

Found this helpful? Don’t forgot to clap 👏 and follow me for more such useful articles about Android development and Kotlin or buy us a coffee here

If you need any help related to Mobile app development. I’m always happy to help you.

Follow me on:

LinkedIn, Medium, Github, Instagram , YouTube & WhatsApp

#AI #Android #Jetpack #Kotlin