Building Advanced Compose UIs
1. Creating Custom Layouts
Custom layouts in Jetpack Compose allow you to define your own layout behavior by combining existing composables or creating entirely new layout structures.
| Property | Description | Usage |
|---|---|---|
| Arrangement | Defines how items are arranged in a layout (horizontal or vertical) | Arrangement.spacedBy(16.dp) |
| Alignment | Defines how items are aligned within a layout | Alignment.CenterHorizontally |
| LayoutScope | Interface that provides layout information to custom layouts | LayoutScope is used in BoxScope, RowScope, etc. |
Example: Custom Vertical Layout
fun CustomVerticalLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
var yPosition = 0
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, y = yPosition)
yPosition += placeable.height
}
}
}
}
Example: Custom Horizontal Layout
fun CustomHorizontalLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
var xPosition = 0
placeables.forEach { placeable ->
placeable.placeRelative(x = xPosition, y = 0)
xPosition += placeable.width
}
}
}
}
2. Implementing Intrinsic Measurements
In Compose, intrinsic measurements refer to the natural size of a composable based on its content. These measurements are crucial for layouts that need to respect the size of their children without forcing a specific dimension.
| Measurement | Description | Usage |
|---|---|---|
| IntrinsicWidth | Defines the minimum and maximum width a composable can take based on its content. | Used in custom layout structures to define horizontal constraints. |
| IntrinsicHeight | Defines the minimum and maximum height a composable can take based on its content. | Used in custom layout structures to define vertical constraints. |
Example: Custom Composable with Intrinsic Measurements
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.layout.*
import androidx.compose.ui.unit.*
@Composable
fun CustomTextWithIntrinsicSize(text: String) {
val intrinsicSize = remember(text) {
LayoutIdMeasurePolicy { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val width = placeables.maxOfOrNull { it.width } ?: 0
val height = placeables.maxOfOrNull { it.height } ?: 0
layout(width, height) {
placeables.forEachIndexed { index, placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
Layout(
content = {
Text(text = text)
},
measurePolicy = intrinsicSize
)
}
3. Using SubcomposeLayout
SubcomposeLayout is a powerful Compose layout that allows you to dynamically compose content based on the available space and other constraints. It's particularly useful for creating complex layouts where the content needs to be composed in a specific order or based on runtime conditions.
| Property | Description | Usage |
|---|---|---|
| subcompose | Defines the content to be composed dynamically. | subcompose { /* Compose content */ } |
| measurePolicy | Defines how to measure and layout the content. | measurePolicy { /* Measure logic */ } |
Example: Using SubcomposeLayout
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.unit.*
@Composable
fun SubcomposeExample() {
SubcomposeLayout(
modifier = Modifier.fillMaxSize(),
measurePolicy = { constraints ->
val placeables = subcompose(LayoutId) {
Text("Dynamic Content")
}.map { it.measure(constraints) }
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach { it.place(0, 0) }
}
}
)
}
SubcomposeLayout is an advanced layout that should be used sparingly, as it can impact performance if not implemented correctly.
4. Implementing ConstraintLayout in Compose
| Property | Description | Values |
|---|---|---|
| constraintSet | Defines the constraints between composables | ConstraintSet |
| applyConstraints | Applies the defined constraints to a composable | ConstraintSet |
Example: Basic ConstraintLayout Setup
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.layout.*
@Composable
fun ConstraintLayoutExample() {
ConstraintLayout(
modifier = Modifier.fillMaxSize()
) {
val (text, button) = createRefs()
Text(
text = "Hello",
modifier = Modifier.constrainAs(text) {
top.linkTo(parent.top, margin = 16.dp)
start.linkTo(parent.start, margin = 16.dp)
}
)
Button(
onClick = { },
modifier = Modifier.constrainAs(button) {
top.linkTo(text.bottom, margin = 16.dp)
end.linkTo(parent.end, margin = 16.dp)
}
) {
Text("Click")
}
}
}
5. Creating Custom Modifiers
Custom modifiers in Jetpack Compose allow you to extend the functionality of existing composables by adding new layout behaviors, drawing operations, or interaction handling.
| Property | Description | Usage |
|---|---|---|
| Modifier | Base interface for all modifiers in Jetpack Compose | Implement or extend to create custom behavior |
| Modifier.composed | Factory method to create a composed modifier | Use for creating modifiers that depend on composition |
Example: Custom Padding Modifier
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
fun Modifier.customPadding(
start: Dp = 0.dp,
top: Dp = 0.dp,
end: Dp = 0.dp,
bottom: Dp = 0.dp
): Modifier {
return this.then(
Modifier.padding(
start = start,
top = top,
end = end,
bottom = bottom
)
)
}
@Composable
fun CustomPaddingExample() {
Box(
modifier = Modifier
.customPadding(start = 16.dp, top = 8.dp)
.size(100.dp)
) {
Text("Padded Content")
}
}
Example: Custom Border Modifier
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.unit.dp
fun Modifier.customBorder(
color: Color = Color.Black,
width: Dp = 1.dp
): Modifier {
return this.then(
Modifier.drawWithCache {
onDrawBehind {
val borderStroke = BorderStroke(width.value, SolidColor(color))
drawRect(
color = Color.Transparent,
size = size,
style = androidx.compose.ui.graphics.drawscope.DrawStyle.Stroke(
width = borderStroke.width.value,
color = borderStroke.color
)
)
}
}
)
}
@Composable
fun BorderExample() {
Box(
modifier = Modifier
.customBorder(color = Color.Red, width = 2.dp)
.size(100.dp)
) {
Text("Bordered Content")
}
}
6. Building Reusable Components
Reusable components in Jetpack Compose allow you to build modular UIs by encapsulating logic and layout into composable functions that can be reused across your app.
| Component | Description | Usage |
|---|---|---|
| @Composable | Marker for functions that build UI elements | @Composable fun MyComponent() { ... } |
| State Hoisting | Move state outside of component to allow reusability | var value by remember { mutableStateOf(0) } |
Example: Reusable Button Component
@Composable
fun CustomButton(
text: String,
onClick: () -> Unit,
enabled: Boolean = true
) {
Button(
onClick = onClick,
enabled = enabled,
modifier = Modifier.padding(8.dp)
) {
Text(text = text)
}
}
Example: Reusable Card Component
@Composable
fun CustomCard(
title: String,
content: String,
onClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
onClick = onClick
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.h6
)
Text(
text = content,
style = MaterialTheme.typography.body2
)
}
}
}
Modifier and remember to ensure components are efficient and state is properly managed.Best Practices for Reusable Components
- Use
@Composablefunctions to define UI elements - Pass in parameters for dynamic content and behavior
- Use
Modifierto allow customization of layout and appearance - Manage state at a higher level to keep components stateless
7. Implementing Gesture Handling
| Gesture | Description | Usage |
|---|---|---|
| TapGesture | Handles single tap gestures | Modifier.clickable { /* Handle tap */ } |
| LongPressGesture | Handles long press gestures | Modifier.longPressGestureFilter { /* Handle long press */ } |
| DragGesture | Handles drag gestures | Modifier.draggable { /* Handle drag */ } |
| FlingGesture | Handles fling gestures for scrollable content | Modifier.flingBehavior() |
Example: Tap Gesture
Box(
modifier = Modifier
.size(100.dp)
.clickable {
Log.d("Gesture", "Box tapped")
}
)
Example: Drag Gesture
var offset by remember { mutableStateOf(Offset.Zero) }
Box(
modifier = Modifier
.size(100.dp)
.offset { IntOffset(offset.x.toInt(), offset.y.toInt()) }
.draggable(
onDrag = { delta ->
offset += delta
},
onDragEnd = {
// Handle drag end logic
}
)
)
Modifier.pointerInput for more complex gesture handling.
8. Using Nested Scrolling
Nested scrolling in Jetpack Compose allows content to scroll within another scrollable container, enabling complex UIs like lists inside swipeable cards or tabbed layouts.
| Property | Description | Usage |
|---|---|---|
| nestedScroll | Applies nested scroll handling to a composable | nestedScroll(rememberNestedScrollInterop()) |
| NestableScrollConnection | Interface to define scroll behavior for nested content | Implement onPreScroll, onPostScroll |
Example: Nested Scroll in List
Box(
modifier = Modifier
.fillMaxSize()
.nestedScroll(rememberNestedScrollInterop())
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.nestedScroll(rememberNestedScrollInterop())
) {
items(100) { index ->
Text(
text = "Item $index",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
}
}
nestedScroll must be applied to both parent and child composables for proper behavior.
9. Creating Compose Animations
Example: Basic Fade Animation
Box(
modifier = Modifier
.size(100.dp)
.animateColorAsState(
target = if (visible) Color.Blue else Color.Red,
animationSpec = tween(300)
)
)
| Property | Description | Values |
|---|---|---|
| animateColorAsState | Animates color changes over time | target: Color, animationSpec: AnimationSpec |
| animateContentSize | Animates content size changes | animationSpec: AnimationSpec |
| animateFloatAsState | Animates float values | target: Float, animationSpec: AnimationSpec |
Example: Scale Animation
val scale by animateFloatAsState(
targetValue = if (isExpanded) 1.2f else 1f,
animationSpec = tween(300)
)
Box(
modifier = Modifier
.size(100.dp)
.scale(scale)
)
tween for smooth transitions, or spring for bouncy effects.Example: Custom Easing Function
val progress by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 500,
easing = FastOutSlowInEasing
)
)
Animation Types Overview
- tween: Smooth transitions over time
- spring: Bouncy, natural motion
- repeatable: Looping animations
10. Implementing CompositionLocal
CompositionLocal is a powerful feature in Jetpack Compose that allows you to provide values to composables in a hierarchical manner without passing them explicitly through parameters. It's particularly useful for sharing configuration, themes, or other values across a composable tree.
| Property | Description | Usage |
|---|---|---|
| CompositionLocal | Represents a local value that can be read by composables in the hierarchy. | val LocalTheme = compositionLocalOf { Theme() } |
| compositionLocalOf | Creates a new CompositionLocal with the specified default value. |
val LocalTheme = compositionLocalOf { Theme() } |
| provides | Used to provide a value for a CompositionLocal in a composable. |
LocalTheme.provides(theme) { ... } |
Example: Defining and Using CompositionLocal
import androidx.compose.runtime.*
// Define a CompositionLocal for theme
val LocalTheme = compositionLocalOf { Theme() }
// Define a Theme data class
data class Theme(
val primaryColor: Color = Color.Blue,
val fontSize: Dp = 16.dp
)
// Provide a theme value
@Composable
fun ThemeProvider(
theme: Theme,
content: @Composable () -> Unit
) {
CompositionLocalProvider(LocalTheme provides theme) {
content()
}
}
// Using the local theme in a composable
@Composable
fun ThemedText(text: String) {
val theme = LocalTheme.current
Text(
text = text,
color = theme.primaryColor,
fontSize = theme.fontSize
)
}
CompositionLocal values are read at composition time, so changes to the value will not automatically recompose composables that depend on it unless they are part of a remember block or another recomposition trigger.
Key Points
CompositionLocalis a way to pass values down the composable hierarchy without explicit parameters.- Use
compositionLocalOfto define a new local value. - Use
CompositionLocalProviderto provide a value for a local. - Access the current value using
LocalName.current.
11. Using Shared Element Transitions
Shared element transitions allow you to animate a UI element between two composables, creating a seamless visual connection as the content changes.
| Property | Description | Values |
|---|---|---|
| sharedElement | Defines the shared element transition | SharedTransitionScope |
| transitionName | Unique identifier for the shared element | String |
Example: Defining a Shared Element Transition
val sharedTransitionScope = rememberSharedTransitionScope()
val transitionName = "sharedElement"
Example: Using Shared Element in Composable
@Composable
fun SharedElementExample() {
val sharedTransitionScope = rememberSharedTransitionScope()
val transitionName = "sharedElement"
Box(
modifier = Modifier
.fillMaxSize()
.sharedElement(
sharedTransitionScope,
transitionName = transitionName
)
) {
// Content here
}
}
Example: Transitioning Between Composables
val sharedTransitionScope = rememberSharedTransitionScope()
val transitionName = "sharedElement"
// First Composable
Box(
modifier = Modifier
.fillMaxSize()
.sharedElement(
sharedTransitionScope,
transitionName = transitionName
)
)
// Second Composable
Box(
modifier = Modifier
.fillMaxSize()
.sharedElement(
sharedTransitionScope,
transitionName = transitionName
)
)
12. Optimizing Compose Performance
| Optimization Technique | Description | Usage |
|---|---|---|
| LazyColumn / LazyRow | Efficiently render large lists by only composing visible items. | |
| Recomposition | Recompute only changed parts of the UI when state changes. | val count by remember { mutableStateOf(0) } |
| State Hoisting | Move state to a higher level to reduce recompositions in child composables. | val count by remember { mutableStateOf(0) } |
| SnapshotStateList | Use for mutable lists that need to trigger recompositions. | val items = remember { mutableStateListOf("Item1", "Item2") } |
Example: Using LazyColumn for Performance
LazyColumn {
items(1000) { index ->
Text(text = "Item $index")
}
}
Example: Avoiding Unnecessary Recompositions
fun MyComposable() {
var count by remember { mutableStateOf(0) }
Column {
Text(text = "Count: $count")
Button(onClick = { count++ }) {
Text(text = "Increment")
}
}
}
remember to cache expensive computations and avoid recomputing values unnecessarily.