Skip to main content

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
            }
        }
    }
}
Note: Custom layouts should be used sparingly as they can impact performance if not implemented efficiently.
Warning: Layouts must properly handle constraints to ensure correct rendering on all screen sizes.

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
    )
}
Note: Intrinsic measurements are particularly useful when building custom layouts or when you want to ensure that a composable respects its content's natural dimensions.

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) }
            }
        }
    )
}
Note: 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")
        }
    }
}
Note: ConstraintLayout in Jetpack Compose is a powerful layout system that allows you to define complex layouts with constraints between composables.

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
            )
        }
    }
}
Note: Use Modifier and remember to ensure components are efficient and state is properly managed.

Best Practices for Reusable Components

  • Use @Composable functions to define UI elements
  • Pass in parameters for dynamic content and behavior
  • Use Modifier to 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
            }
        )
)
Note: Use 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)
            )
        }
    }
}
Note: nestedScroll must be applied to both parent and child composables for proper behavior.
Warning: Improper use of nested scroll can lead to performance issues or unexpected scrolling 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)
)
Note: Use 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
    )
}
Note: 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

  • CompositionLocal is a way to pass values down the composable hierarchy without explicit parameters.
  • Use compositionLocalOf to define a new local value.
  • Use CompositionLocalProvider to 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.
LazyColumn {
    items(itemsList) { item ->
        Text(text = item)
    }
}
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")
        }
    }
}
Note: Use remember to cache expensive computations and avoid recomputing values unnecessarily.
Warning: Avoid placing heavy logic inside composables that are frequently recomposed.

Performance Best Practices

  • Use LazyColumn or LazyRow for large lists.
  • Minimize recompositions by hoisting state.
  • Avoid recomputing values inside composables.
  • Use remember for expensive operations or state.

Was this article helpful?