Jetpack Compose is revolutionizing UI development, not just for Android but also for various platforms, thanks to Compose Multiplatform. One of the key aspects of building efficient and appealing applications is mastering layouts. In this blog post, we’ll dive into creating cross-platform layouts using Jetpack Compose Multiplatform.
Understanding Compose Multiplatform
Compose Multiplatform, developed by JetBrains, extends Jetpack Compose to enable UI development for Android, iOS, desktop (JVM), and web, all from a single codebase. This means you can write your UI once and deploy it across different platforms, reducing development time and costs.
Why Use Compose Multiplatform Layouts?
- Code Reusability: Write layouts once and use them across different platforms.
- Consistent UI: Maintain a consistent look and feel across all your applications.
- Simplified Development: Reduce platform-specific code, making development faster and more efficient.
Setting Up a Compose Multiplatform Project
Before diving into layouts, let’s set up a basic Compose Multiplatform project.
Step 1: Create a New Project
Use the Kotlin Multiplatform wizard in IntelliJ IDEA to create a new project with the desired platforms (Android, iOS, desktop, and web). Alternatively, use the command-line tool.
Step 2: Project Structure
Your project structure should look something like this:
MyComposeApp/
├── androidApp/ # Android application
├── iosApp/ # iOS application
├── desktopApp/ # Desktop application
├── jsApp/ # Web application
├── shared/ # Shared Kotlin code
├── src/commonMain/ # Common source set for all platforms
├── src/androidMain/ # Android-specific code
├── src/iosMain/ # iOS-specific code
├── src/jvmMain/ # Desktop-specific code
└── src/jsMain/ # Web-specific code
Basic Layouts in Compose Multiplatform
Jetpack Compose offers several basic layout composables that work seamlessly across platforms:
Column
: Arranges items vertically.Row
: Arranges items horizontally.Box
: Places items on top of each other.
Example: Creating a Simple Vertical Layout
Let’s create a simple layout using Column
that displays a title and a description.
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun SimpleColumnLayout() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "My App Title", style = MaterialTheme.typography.h5)
Text(text = "A simple description of my app.")
}
}
This layout is now reusable across all platforms. Simply call SimpleColumnLayout()
in your platform-specific UI.
Adaptive Layouts
To create layouts that adapt to different screen sizes and orientations, you can use the BoxWithConstraints
composable.
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.layout.Layout
@Composable
fun AdaptiveLayout() {
BoxWithConstraints {
val screenWidth = maxWidth
if (screenWidth < 600.dp) {
// Small screen: Display in a single column
Column(
modifier = Modifier.padding(16.dp)
) {
Text("Small Screen Layout")
Text("Content for small screens")
}
} else {
// Large screen: Display in a row
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("Large Screen Layout")
Text("Content for large screens")
}
}
}
}
In this example, the layout adapts based on the screen width. On smaller screens (like mobile phones), it uses a Column
, and on larger screens (like tablets or desktops), it uses a Row
.
Custom Layouts
For more complex layouts, you might need to create custom layouts. Here’s how you can do it using the Layout
composable.
Example: Creating a Simple Grid Layout
Let's create a basic grid layout.
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.Constraints
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Card
import androidx.compose.material.Text
@Composable
fun GridLayout(
modifier: Modifier = Modifier,
columns: Int = 2,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
val columnWidth = constraints.maxWidth / columns
val itemConstraints = Constraints.fixedWidth(columnWidth)
val placeables = measurables.map { measurable ->
measurable.measure(itemConstraints)
}
layout(
width = constraints.maxWidth,
height = placeables.sumOf { it.height } / columns + 50 // Approximate height calculation
) {
var xPosition = 0
var yPosition = 0
var column = 0
placeables.forEach { placeable ->
placeable.placeRelative(x = xPosition, y = yPosition)
xPosition += columnWidth
column++
if (column >= columns) {
xPosition = 0
yPosition += placeable.height
column = 0
}
}
}
}
}
@Composable
fun GridExample() {
GridLayout(columns = 3, modifier = Modifier.padding(16.dp)) {
repeat(6) { index ->
Card(modifier = Modifier.padding(8.dp)) {
Text(text = "Item $index", modifier = Modifier.padding(16.dp))
}
}
}
}
In this example:
GridLayout
takes acolumns
parameter to define the number of columns.- It measures each child with a fixed width and places them in a grid-like structure.
- The height calculation is approximate but can be adjusted based on the content.
Advanced Layouts with ConstraintLayout
For complex and responsive layouts, ConstraintLayout
provides a powerful solution. Though the common usage may slightly vary across platforms, the core concepts remain consistent.
import androidx.compose.foundation.layout.ConstraintLayout
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.layout.layoutId
import androidx.compose.foundation.layout.ConstraintSet
import androidx.compose.ui.unit.dp
@Composable
fun ConstraintLayoutExample() {
ConstraintLayout {
// Create references for the composables to constrain
val (text1, text2) = createRefs()
Text(
text = "Text 1",
modifier = Modifier.constrainAs(text1) {
top.linkTo(parent.top, margin = 16.dp)
start.linkTo(parent.start, margin = 16.dp)
}
)
Text(
text = "Text 2",
modifier = Modifier.constrainAs(text2) {
top.linkTo(text1.bottom, margin = 8.dp)
end.linkTo(parent.end, margin = 16.dp)
}
)
}
}
With ConstraintLayout
, you can define relationships between different composables, making your layouts highly flexible and responsive.
Tips for Compose Multiplatform Layouts
- Use Common Source Set: Keep as much layout code as possible in the
commonMain
source set to maximize code reuse. - Adaptive Design: Use
BoxWithConstraints
and other techniques to create adaptive layouts that work well on different screen sizes and orientations. - Platform-Specific Adjustments: For UI elements, that require subtle platform-specific changes, consider conditional compilation or delegation with an `expect`/`actual` pattern.
- Testing: Test your layouts thoroughly on all target platforms to ensure they look and behave as expected.
Conclusion
Compose Multiplatform is a powerful tool for building cross-platform applications with a single codebase. By mastering layouts in Compose, you can create visually appealing and responsive UIs that work seamlessly on Android, iOS, desktop, and web. Experiment with basic layouts, adaptive designs, custom layouts, and ConstraintLayout
to build the best possible user experience across all your platforms. By carefully planning your architectural approach, leveraging common patterns in KMP, you will drastically improve development time and cost.