Creating Responsive UIs with Jetpack Compose

Creating a responsive user interface (UI) is crucial for modern Android applications. A responsive UI adapts seamlessly to different screen sizes, orientations, and densities, providing an optimal user experience across various devices. Jetpack Compose simplifies the process of building such UIs with its declarative approach and flexible layout system.

What is a Responsive UI?

A responsive UI is a user interface that automatically adjusts its layout, size, and content to fit the screen on which it is displayed. It ensures that the application looks and functions correctly across a wide range of devices, from smartphones and tablets to foldable devices and large-screen displays.

Why Create Responsive UIs?

  • Improved User Experience: Provides an optimal viewing experience regardless of the device.
  • Wider Audience Reach: Supports users on different devices, increasing accessibility.
  • Maintainability: Reduces the need for separate layouts for each screen size, simplifying development and maintenance.

How to Create Responsive UIs with Jetpack Compose

Jetpack Compose offers several features and techniques for creating responsive UIs.

1. Using Modifier.fillMaxSize() and Modifier.fillMaxWidth()

These modifiers allow Composables to take up available space within their parent layout. fillMaxSize() makes a Composable fill the entire available space of its parent, while fillMaxWidth() makes it fill the available width.


import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun FillMaxSizeExample() {
    Column(modifier = Modifier.fillMaxSize()) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f)
                .background(Color.Red),
            contentAlignment = Alignment.Center
        ) {
            Text("Top Section - fillMaxWidth, weight=1")
        }

        Box(
            modifier = Modifier
                .fillMaxWidth()
                .weight(2f)
                .background(Color.Green),
            contentAlignment = Alignment.Center
        ) {
            Text("Bottom Section - fillMaxWidth, weight=2")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun FillMaxSizePreview() {
    FillMaxSizeExample()
}

In this example:

  • The Column takes up the entire screen using fillMaxSize().
  • The fillMaxWidth modifier in combination with weight divide available space proportionally

2. Using BoxWithConstraints

BoxWithConstraints provides the constraints of the parent layout, allowing you to adjust the UI based on available space.


import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun BoxWithConstraintsExample() {
    BoxWithConstraints {
        if (maxWidth < 600.dp) {
            Text("Small screen", modifier = Modifier.align(Alignment.Center))
        } else {
            Text("Large screen", modifier = Modifier.align(Alignment.Center))
        }
    }
}

@Preview(showBackground = true)
@Composable
fun BoxWithConstraintsPreview() {
    BoxWithConstraintsExample()
}

This code:

  • Uses BoxWithConstraints to get the maximum width of the available space.
  • Changes the text based on whether the screen width is less than 600dp.

3. Adaptive Layouts with Row and Column

Combine Row and Column to create layouts that adapt to different screen orientations and sizes.


import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.*

@Composable
fun AdaptiveLayoutExample() {
    Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
        Column(modifier = Modifier.weight(1f).padding(8.dp)) {
            Text("Column 1 - content")
            Button(onClick = {}) {
                Text("Button 1")
            }
        }
        Column(modifier = Modifier.weight(1f).padding(8.dp)) {
            Text("Column 2 - content")
            Button(onClick = {}) {
                Text("Button 2")
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun AdaptiveLayoutPreview() {
    AdaptiveLayoutExample()
}

In this example:

  • A Row contains two Column elements.
  • The weight modifier makes the columns share available width equally.

4. Using ConstraintLayout for Complex Scenarios

ConstraintLayout offers more complex layout options, allowing you to position Composables relative to each other and to the parent.


import androidx.compose.foundation.background
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.constraintlayout.compose.ConstraintLayout

@Composable
fun ConstraintLayoutExample() {
    ConstraintLayout {
        val (text1, text2) = createRefs()

        Text(
            "Text 1",
            modifier = Modifier
                .constrainAs(text1) {
                    top.linkTo(parent.top, margin = 16.dp)
                    start.linkTo(parent.start, margin = 16.dp)
                }
                .background(Color.Yellow)
        )

        Text(
            "Text 2",
            modifier = Modifier
                .constrainAs(text2) {
                    top.linkTo(text1.bottom, margin = 8.dp)
                    end.linkTo(parent.end, margin = 16.dp)
                }
                .background(Color.Cyan)
        )
    }
}

@Preview(showBackground = true)
@Composable
fun ConstraintLayoutPreview() {
    ConstraintLayoutExample()
}

Key points:

  • createRefs() creates references to the Composables to be constrained.
  • Each Text composable uses constrainAs to define its constraints relative to other Composables and the parent.

5. Handling Different Screen Orientations

Adapt your UI to different screen orientations by checking the orientation configuration and adjusting the layout accordingly.


import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import android.content.res.Configuration
import androidx.compose.ui.platform.LocalConfiguration

@Composable
fun OrientationAwareLayout() {
    val configuration = LocalConfiguration.current

    if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        // Landscape layout
        Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
            Text("Landscape Mode", modifier = Modifier.padding(8.dp))
            Button(onClick = {}) {
                Text("Action")
            }
        }
    } else {
        // Portrait layout
        Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
            Text("Portrait Mode", modifier = Modifier.padding(8.dp))
            Button(onClick = {}) {
                Text("Action")
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun OrientationAwareLayoutPreview() {
    OrientationAwareLayout()
}

In this code:

  • LocalConfiguration.current provides access to the device configuration.
  • Based on the orientation, different layouts are displayed.

6. Using Density-Aware Modifiers

Adjust UI elements based on screen density to maintain consistent visual appearance across devices.


import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun DensityAwareText() {
    val density = LocalDensity.current.density
    val textSize = 16 * density // Example: adjust text size based on density

    Text("Density-Aware Text", modifier = Modifier)
}

@Preview(showBackground = true)
@Composable
fun DensityAwareTextPreview() {
    DensityAwareText()
}

Conclusion

Creating responsive UIs in Jetpack Compose involves utilizing a combination of flexible layouts, constraints, and adaptive modifiers. By leveraging techniques such as fillMaxSize(), BoxWithConstraints, adaptive Row and Column layouts, ConstraintLayout, handling different screen orientations, and density-aware modifiers, you can ensure that your application provides a seamless and visually appealing experience across a wide range of Android devices. Properly implemented responsive design greatly enhances the usability and overall quality of your Android applications.