If you’ve ever opened your beautifully designed app on a tablet only to see it looking like a stretched-out phone screen, you know the pain. Or maybe you’ve watched your carefully crafted layout break apart on a foldable device. Trust me, I’ve been there.
The good news..? Responsive and Adaptive UI in Jetpack Compose makes handling multiple screen sizes way easier than the old XML days. Today, I’m going to walk you through everything you need to know to make your apps look fantastic on every device, from tiny phones to massive tablets.
Let’s dive in!
What’s the Difference Between Responsive and Adaptive UI?
Before we jump into code, let’s clear up some confusion. These terms get thrown around a lot, but they mean different things.
Responsive UI is like water — it flows and adjusts smoothly to fit any container. Your layout stretches, shrinks, and rearranges based on available space. Think of a text field that grows wider on a tablet or a grid that shows more columns on larger screens.
Adaptive UI is more like having different outfits for different occasions. Your app actually changes its structure based on the device. On a phone, you might show a single-pane layout. On a tablet, you’d show a master-detail view with two panes side by side.
In real-world apps, you’ll use both approaches together. That’s what makes Responsive and Adaptive UI in Jetpack Compose so powerful.
Why Jetpack Compose Makes This Easier
If you’ve built responsive layouts in XML, you know it can get messy fast. Multiple layout files, qualifiers, configuration changes — it’s a lot to manage.
Jetpack Compose changes the game. Everything is code-based, which means you can use regular Kotlin logic to make decisions about your UI. No more jumping between files. No more cryptic folder names like layout-sw600dp-land.
Plus, Compose gives you real-time information about screen size, orientation, and window metrics. You can make smart decisions on the fly.
Setting Up Your Project
First things first. Make sure you have the necessary dependencies in your build.gradle.kts file:
dependencies {
implementation("androidx.compose.ui:ui:1.6.0")
implementation("androidx.compose.material3:material3:1.2.0")
implementation("androidx.compose.material3:material3-window-size-class:1.2.0")
}The material3-window-size-class library is your best friend for building Responsive and Adaptive UI in Jetpack Compose. It provides a standardized way to categorize screen sizes.
Understanding Window Size Classes
Window size classes are Google’s recommended way to handle different screen sizes. Instead of checking exact pixel dimensions, you work with three categories: Compact, Medium, and Expanded.
Here’s what they mean:
- Compact: Most phones in portrait mode (width < 600dp)
- Medium: Most phones in landscape, small tablets, or foldables (600dp ≤ width < 840dp)
- Expanded: Large tablets and desktops (width ≥ 840dp)
Let’s see how to get the current window size class:
@Composable
fun MyResponsiveApp() {
val windowSizeClass = calculateWindowSizeClass(activity = this as Activity)
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> {
// Show phone layout
CompactLayout()
}
WindowWidthSizeClass.Medium -> {
// Show tablet or landscape phone layout
MediumLayout()
}
WindowWidthSizeClass.Expanded -> {
// Show large tablet or desktop layout
ExpandedLayout()
}
}
}Here, we’re calculating the window size class and using a when statement to decide which layout to show. Simple, right..? This is the foundation of adaptive UI.
You can also check height size classes the same way using windowSizeClass.heightSizeClass. This is super useful for handling landscape orientations.
Building Your First Responsive Layout
Let’s start with something practical — a responsive grid that adjusts the number of columns based on screen size.
@Composable
fun ResponsiveGrid(
items: List<String>,
windowSizeClass: WindowSizeClass
) {
// Determine columns based on screen width
val columns = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> 2
WindowWidthSizeClass.Medium -> 3
WindowWidthSizeClass.Expanded -> 4
else -> 2
}
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(items) { item ->
GridItem(text = item)
}
}
}
@Composable
fun GridItem(text: String) {
Card(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = text, style = MaterialTheme.typography.bodyLarge)
}
}
}We’re creating a grid that shows 2 columns on phones, 3 on medium devices, and 4 on large tablets. The LazyVerticalGrid handles the layout, and we’re using GridCells.Fixed() to set the column count.
The aspectRatio(1f) modifier makes each card square, which looks clean and consistent across all screen sizes. The spacing and padding ensure everything breathes nicely.
Creating Adaptive Navigation
Navigation is where adaptive UI really shines. On phones, you typically use a bottom navigation bar or navigation drawer. On tablets, a persistent navigation rail makes better use of space.
Here’s how to implement adaptive navigation:
@Composable
fun AdaptiveNavigationLayout(
windowSizeClass: WindowSizeClass,
currentDestination: String,
onNavigate: (String) -> Unit,
content: @Composable () -> Unit
) {
val useNavigationRail = windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact
if (useNavigationRail) {
// Tablet layout with navigation rail
Row(modifier = Modifier.fillMaxSize()) {
NavigationRail(
modifier = Modifier.fillMaxHeight()
) {
NavigationItems(
currentDestination = currentDestination,
onNavigate = onNavigate,
isRail = true
)
}
Box(modifier = Modifier.weight(1f)) {
content()
}
}
} else {
// Phone layout with bottom navigation
Scaffold(
bottomBar = {
NavigationBar {
NavigationItems(
currentDestination = currentDestination,
onNavigate = onNavigate,
isRail = false
)
}
}
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
content()
}
}
}
}
@Composable
fun RowScope.NavigationItems(
currentDestination: String,
onNavigate: (String) -> Unit,
isRail: Boolean
) {
val items = listOf("Home", "Search", "Profile")
items.forEach { item ->
if (isRail) {
NavigationRailItem(
icon = { Icon(getIconForItem(item), contentDescription = item) },
label = { Text(item) },
selected = currentDestination == item,
onClick = { onNavigate(item) }
)
} else {
NavigationBarItem(
icon = { Icon(getIconForItem(item), contentDescription = item) },
label = { Text(item) },
selected = currentDestination == item,
onClick = { onNavigate(item) }
)
}
}
}On compact screens (phones), we use NavigationBar at the bottom. On medium and expanded screens, we use NavigationRail on the side. The useNavigationRail boolean makes this decision.
The Row layout for tablets puts the navigation rail on the left and gives the content area the remaining space with weight(1f). Clean and efficient!
Master-Detail Pattern for Tablets
The master-detail pattern is the gold standard for tablet layouts. You show a list on the left and details on the right. On phones, you navigate between these screens.
@Composable
fun MasterDetailLayout(
items: List<Item>,
selectedItem: Item?,
onItemSelected: (Item) -> Unit,
windowSizeClass: WindowSizeClass
) {
val showTwoPane = windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact
if (showTwoPane) {
// Two-pane layout for tablets
Row(modifier = Modifier.fillMaxSize()) {
// Master pane
Box(
modifier = Modifier
.weight(0.4f)
.fillMaxHeight()
) {
ItemList(
items = items,
selectedItem = selectedItem,
onItemSelected = onItemSelected
)
}
// Divider
VerticalDivider()
// Detail pane
Box(
modifier = Modifier
.weight(0.6f)
.fillMaxHeight()
) {
if (selectedItem != null) {
ItemDetail(item = selectedItem)
} else {
EmptyDetailView()
}
}
}
} else {
// Single-pane layout for phones
if (selectedItem != null) {
ItemDetail(
item = selectedItem,
onBackPressed = { onItemSelected(null) }
)
} else {
ItemList(
items = items,
selectedItem = null,
onItemSelected = onItemSelected
)
}
}
}
@Composable
fun ItemList(
items: List<Item>,
selectedItem: Item?,
onItemSelected: (Item) -> Unit
) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(items) { item ->
ListItem(
headlineContent = { Text(item.title) },
supportingContent = { Text(item.description) },
modifier = Modifier
.clickable { onItemSelected(item) }
.background(
if (item == selectedItem)
MaterialTheme.colorScheme.primaryContainer
else
Color.Transparent
)
)
HorizontalDivider()
}
}
}
@Composable
fun ItemDetail(item: Item, onBackPressed: (() -> Unit)? = null) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
onBackPressed?.let {
IconButton(onClick = it) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
Text(
text = item.title,
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
Text(
text = item.description,
style = MaterialTheme.typography.bodyLarge
)
}
}On phones, we show either the list or the detail screen — never both. When an item is selected, we navigate to the detail view and provide a back button.
On tablets, both panes are visible simultaneously. The list takes 40% of the width (weight(0.4f)), and the detail pane takes 60% (weight(0.6f)). Clicking an item updates the detail pane without any navigation.
This is Responsive and Adaptive UI in Jetpack Compose at its finest!
Using BoxWithConstraints for Fine-Grained Control
Sometimes you need more precise control over your layout based on exact dimensions. That’s where BoxWithConstraints comes in handy.
@Composable
fun FlexibleLayout() {
BoxWithConstraints(
modifier = Modifier.fillMaxSize()
) {
// maxWidth and maxHeight are available here
val isWideScreen = maxWidth > 600.dp
val isLandscape = maxWidth > maxHeight
if (isWideScreen && isLandscape) {
// Landscape tablet layout
Row(modifier = Modifier.fillMaxSize()) {
Sidebar(modifier = Modifier.width(280.dp))
MainContent(modifier = Modifier.weight(1f))
}
} else if (isWideScreen) {
// Portrait tablet layout
Column(modifier = Modifier.fillMaxSize()) {
TopBar(modifier = Modifier.height(80.dp))
MainContent(modifier = Modifier.weight(1f))
}
} else {
// Phone layout
Column(modifier = Modifier.fillMaxSize()) {
CompactTopBar(modifier = Modifier.height(56.dp))
MainContent(modifier = Modifier.weight(1f))
}
}
}
}Why is this useful? BoxWithConstraints gives you the exact constraints (min/max width and height) of your composable. You can make pixel-perfect decisions about your layout.
The content inside BoxWithConstraints recomposes whenever the constraints change, like when the device rotates or the window is resized. This makes it perfect for Responsive and Adaptive UI in Jetpack Compose.
Responsive Typography and Spacing
Don’t forget about text sizes and spacing! What looks good on a phone might be tiny on a tablet.
@Composable
fun ResponsiveText(
text: String,
windowSizeClass: WindowSizeClass
) {
val textStyle = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> MaterialTheme.typography.bodyMedium
WindowWidthSizeClass.Medium -> MaterialTheme.typography.bodyLarge
WindowWidthSizeClass.Expanded -> MaterialTheme.typography.headlineSmall
else -> MaterialTheme.typography.bodyMedium
}
val horizontalPadding = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> 16.dp
WindowWidthSizeClass.Medium -> 32.dp
WindowWidthSizeClass.Expanded -> 64.dp
else -> 16.dp
}
Text(
text = text,
style = textStyle,
modifier = Modifier.padding(horizontal = horizontalPadding)
)
}The concept: Larger screens can handle bigger text and more generous spacing. This code adjusts both based on the window size class, creating a more comfortable reading experience on every device.
Handling Configuration Changes Gracefully
One beautiful thing about Compose is that it handles configuration changes automatically. When the device rotates or folds, your composables recompose with the new window size class.
But you need to manage state properly:
@Composable
fun ResponsiveApp() {
val windowSizeClass = calculateWindowSizeClass(activity = LocalContext.current as Activity)
// State survives configuration changes with rememberSaveable
var selectedTab by rememberSaveable { mutableStateOf(0) }
var selectedItem by rememberSaveable { mutableStateOf<Item?>(null) }
AdaptiveScaffold(
windowSizeClass = windowSizeClass,
selectedTab = selectedTab,
onTabSelected = { selectedTab = it },
selectedItem = selectedItem,
onItemSelected = { selectedItem = it }
) {
// Your content here
}
}Key point: Use rememberSaveable instead of remember for state that should survive configuration changes. This ensures your selected tab or item doesn’t reset when the user rotates their device.
Testing on Different Screen Sizes
Building Responsive and Adaptive UI in Jetpack Compose is only half the battle. You need to test it too!
Compose makes testing easier with preview annotations:
@Preview(name = "Phone", device = Devices.PHONE)
@Preview(name = "Foldable", device = Devices.FOLDABLE)
@Preview(name = "Tablet", device = Devices.TABLET)
@Preview(name = "Desktop", device = Devices.DESKTOP)
@Composable
fun PreviewResponsiveLayout() {
MaterialTheme {
ResponsiveGrid(
items = List(20) { "Item ${it + 1}" },
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 1280.dp))
)
}
}Pro tip: Add multiple preview annotations to see your layout on different devices simultaneously in Android Studio. This catches issues early and saves you tons of time.
You can also create custom previews for specific dimensions:
@Preview(name = "Small Phone", widthDp = 360, heightDp = 640)
@Preview(name = "Large Tablet", widthDp = 1024, heightDp = 768)
@Composable
fun CustomSizePreviews() {
// Your composable here
}Common Pitfalls to Avoid
1. Hardcoding sizes: Don’t use fixed pixel values like Modifier.width(400.dp) when you can use fillMaxWidth() or weight(). Let Compose do the math.
2. Forgetting about landscape mode: Always test in both portrait and landscape. A layout that works great in portrait might completely break in landscape.
3. Ignoring content density: Large screens don’t just mean “make everything bigger.” Think about optimal content width. A text paragraph shouldn’t stretch across a 12-inch tablet — it becomes hard to read.
4. Not using window size classes: Checking exact pixel dimensions leads to brittle code. Stick with window size classes for more maintainable Responsive and Adaptive UI in Jetpack Compose.
5. Over-engineering: Start simple. You don’t need a different layout for every possible screen size. Compact, medium, and expanded are usually enough.
Advanced Technique: Content-Based Breakpoints
Sometimes you want to switch layouts based on content, not just screen size. Here’s a clever approach:
@Composable
fun ContentAwareLayout(items: List<String>) {
BoxWithConstraints {
// Calculate how many items fit comfortably
val itemWidth = 120.dp
val spacing = 16.dp
val itemsPerRow = (maxWidth / (itemWidth + spacing)).toInt().coerceAtLeast(1)
if (itemsPerRow >= 4) {
// Show grid layout
LazyVerticalGrid(columns = GridCells.Fixed(itemsPerRow)) {
items(items) { item ->
GridItemCard(item)
}
}
} else {
// Show list layout
LazyColumn {
items(items) { item ->
ListItemCard(item)
}
}
}
}
}What makes this special..? Instead of using predefined breakpoints, we calculate how many items can fit based on their desired width. If we can fit 4 or more, we use a grid. Otherwise, we use a list. This adapts beautifully to any screen size.
Making Images Responsive
Images need special attention in responsive layouts:
@Composable
fun ResponsiveImage(
imageUrl: String,
contentDescription: String,
windowSizeClass: WindowSizeClass
) {
val imageModifier = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
WindowWidthSizeClass.Medium -> Modifier
.width(400.dp)
.aspectRatio(4f / 3f)
WindowWidthSizeClass.Expanded -> Modifier
.width(600.dp)
.aspectRatio(16f / 10f)
else -> Modifier.fillMaxWidth()
}
AsyncImage(
model = imageUrl,
contentDescription = contentDescription,
modifier = imageModifier,
contentScale = ContentScale.Crop
)
}We’re adjusting both the size and aspect ratio of images based on screen size. On phones, full-width images look great. On tablets, fixed-width images with appropriate aspect ratios provide better visual balance.
Performance Considerations
Responsive and Adaptive UI in Jetpack Compose can affect performance if you’re not careful. Here are some tips:
Use derivedStateOf for calculations: If you’re computing values based on window size, use derivedStateOf to avoid unnecessary recompositions.
@Composable
fun PerformantResponsiveLayout(windowSizeClass: WindowSizeClass) {
val columns by remember {
derivedStateOf {
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> 2
WindowWidthSizeClass.Medium -> 3
WindowWidthSizeClass.Expanded -> 4
else -> 2
}
}
}
LazyVerticalGrid(columns = GridCells.Fixed(columns)) {
// Grid items
}
}Avoid heavy calculations in composition: Move expensive operations outside the composable or use LaunchedEffect for side effects.
Be smart with previews: Too many preview configurations can slow down Android Studio. Keep them focused on the most important scenarios.
Putting It All Together
Let’s create a complete example that combines everything we’ve learned:
@Composable
fun CompleteResponsiveApp() {
val windowSizeClass = calculateWindowSizeClass(activity = LocalContext.current as Activity)
var selectedScreen by rememberSaveable { mutableStateOf("home") }
var selectedItem by rememberSaveable { mutableStateOf<Item?>(null) }
AdaptiveNavigationLayout(
windowSizeClass = windowSizeClass,
currentDestination = selectedScreen,
onNavigate = { selectedScreen = it }
) {
when (selectedScreen) {
"home" -> HomeScreen(windowSizeClass)
"browse" -> BrowseScreen(
windowSizeClass = windowSizeClass,
selectedItem = selectedItem,
onItemSelected = { selectedItem = it }
)
"profile" -> ProfileScreen(windowSizeClass)
}
}
}
@Composable
fun HomeScreen(windowSizeClass: WindowSizeClass) {
val columns = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> 1
WindowWidthSizeClass.Medium -> 2
WindowWidthSizeClass.Expanded -> 3
else -> 1
}
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize()
) {
items(20) { index ->
FeaturedCard(
title = "Featured Item ${index + 1}",
description = "This is a great item you should check out",
windowSizeClass = windowSizeClass
)
}
}
}
@Composable
fun BrowseScreen(
windowSizeClass: WindowSizeClass,
selectedItem: Item?,
onItemSelected: (Item?) -> Unit
) {
val sampleItems = remember {
List(50) { index ->
Item(
id = index,
title = "Item ${index + 1}",
description = "Description for item ${index + 1}"
)
}
}
MasterDetailLayout(
items = sampleItems,
selectedItem = selectedItem,
onItemSelected = onItemSelected,
windowSizeClass = windowSizeClass
)
}
@Composable
fun ProfileScreen(
windowSizeClass: androidx.compose.material3.windowsizeclass.WindowSizeClass
) {
val padding = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> 16.dp
WindowWidthSizeClass.Medium -> 32.dp
WindowWidthSizeClass.Expanded -> 64.dp
else -> 16.dp
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Profile",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 24.dp)
)
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "User Name",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 8.dp)
)
Text(
text = "[email protected]",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@Composable
fun FeaturedCard(
title: String,
description: String,
windowSizeClass: WindowSizeClass
) {
val cardElevation = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> 2.dp
else -> 4.dp
}
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 8.dp)
)
Text(
text = description,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Composable
fun AdaptiveNavigationLayout(
windowSizeClass: WindowSizeClass,
currentDestination: String,
onNavigate: (String) -> Unit,
content: @Composable () -> Unit
) {
// Determine if we should use navigation rail (for tablets and larger)
val useNavigationRail = windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact
if (useNavigationRail) {
// Tablet/Desktop layout with navigation rail on the left
Row(modifier = Modifier.fillMaxSize()) {
NavigationRail(
modifier = Modifier.fillMaxHeight()
) {
Spacer(modifier = Modifier.height(12.dp))
NavigationRailItems(
currentDestination = currentDestination,
onNavigate = onNavigate
)
}
Box(modifier = Modifier.weight(1f)) {
content()
}
}
} else {
// Phone layout with bottom navigation
Scaffold(
bottomBar = {
NavigationBar {
NavigationBarItems(
currentDestination = currentDestination,
onNavigate = onNavigate
)
}
}
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
content()
}
}
}
}
@Composable
fun MasterDetailLayout(
items: List<Item>,
selectedItem: Item?,
onItemSelected: (Item?) -> Unit,
windowSizeClass: WindowSizeClass
) {
// Determine if we should show two panes (tablet) or one pane (phone)
val showTwoPane = windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact
if (showTwoPane) {
// Two-pane layout for tablets and larger screens
TwoPaneLayout(
items = items,
selectedItem = selectedItem,
onItemSelected = onItemSelected
)
} else {
// Single-pane layout for phones
SinglePaneLayout(
items = items,
selectedItem = selectedItem,
onItemSelected = onItemSelected
)
}
}
data class Item(
val id: Int,
val title: String,
val description: String
)This complete example shows a real app structure with multiple screens, adaptive navigation, responsive grids, and the master-detail pattern. It’s production-ready code that handles phones, tablets, and everything in between.
Best Practices Recap
Let me summarize the key principles for building great Responsive and Adaptive UI in Jetpack Compose:
Use window size classes as your primary decision-making tool. They’re Google’s recommended approach and they work great.
Think in terms of layouts, not devices. Don’t try to detect if something is a “tablet” or “phone.” Focus on the space available and choose the best layout for that space.
Start with the smallest screen and work your way up. It’s easier to add features for larger screens than to remove them for smaller ones.
Test early and often on different screen sizes. Use previews during development and test on real devices before shipping.
Keep content readable. Don’t let text lines stretch across a 12-inch screen. Consider maximum content width even on large displays.
Use flexible layouts like Row, Column, Box with weights and size modifiers rather than hardcoded dimensions.
Remember that orientation matters. A phone in landscape might have more width than a small tablet in portrait.
What About Foldable Devices?
Foldables add another dimension to responsive design. The window size class approach handles them automatically, but you can also detect fold states if needed:
@Composable
fun FoldableAwareLayout() {
val windowSizeClass = calculateWindowSizeClass(activity = LocalContext.current as Activity)
// Window size classes automatically adjust when device unfolds
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> {
// Folded state - show compact layout
CompactLayout()
}
WindowWidthSizeClass.Medium, WindowWidthSizeClass.Expanded -> {
// Unfolded state - show expanded layout
ExpandedLayout()
}
}
}The beauty of this approach is that you don’t need special foldable detection code. The window size class changes automatically when the device folds or unfolds, and your UI adapts accordingly.
Conclusion
Building Responsive and Adaptive UI in Jetpack Compose might seem daunting at first, but once you understand the core concepts, it becomes second nature.
Remember: window size classes are your friends. Use them to make high-level layout decisions. Combine them with BoxWithConstraints when you need fine-grained control. Test on multiple screen sizes. And always think about how your UI will adapt as screens get bigger or smaller.
The great thing about Compose is that it gives you all the tools you need without the complexity of XML layouts and configuration qualifiers. Everything is code, everything is Kotlin, and everything makes sense.
Your users will thank you when your app looks beautiful on their phone, stunning on their tablet, and perfect on their foldable device.
Now go build something awesome..! And remember — responsive design isn’t just about making things fit. It’s about creating the best possible experience for every screen size.
Happy composing..!
