Skip to main content

Building UIs with Jetpack Compose

1. Creating Composable Functions

Property Description Example
@Composable Annotation indicating a function is a composable @Composable fun Greeting(name: String) { ... }
function signature Composable functions can take parameters like regular functions @Composable fun UserCard(user: User) { ... }
state management Use mutableStateOf to manage UI state var count by remember { mutableStateOf(0) }

Example: Basic Composable Function

@Composable
fun WelcomeMessage(name: String) {
    Text(text = "Hello, $name!")
}

Example: Composable with State

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    
    Column(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}
Note: Composable functions must be called within a composable context and cannot return values directly to the UI.

2. Using Basic Composables

Composable Description Usage
Text Renders text on the screen Text(text = "Hello, Compose!")
Button Displays a clickable button Button(onClick = { /* action */ }) { Text("Click me") }
Image Displays an image resource Image(painter = painterResource(id = R.drawable.icon), contentDescription = null)
Box Layout container for arranging composables Box { Text("Inside Box") }
Column Arranges composables vertically Column { Text("Line 1"); Text("Line 2") }
Row Arranges composables horizontally Row { Text("Left"); Text("Right") }

Example: Simple Text Display

import androidx.compose.material.Text
import androidx.compose.runtime.Composable

@Composable
fun Greeting(name: String) {
    Text(text = "Hello, $name!")
}

Example: Button with Action

import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable

@Composable
fun ClickableButton() {
    Button(onClick = { /* Handle click */ }) {
        Text(text = "Press Me")
    }
}
Note: All composables must be marked with @Composable annotation.

3. Implementing Layouts

Example: Basic Column and Row

import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp

@Composable
fun BasicLayout() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(text = "Header")
        Spacer(modifier = Modifier.height(8.dp))
        Text(text = "Content")
        Spacer(modifier = Modifier.height(8.dp))
        Text(text = "Footer")
    }
}
Layout Description Usage
Box Overlapping layout container Use for layered UIs or overlays
Column Vertical arrangement of composables Stack elements vertically
Row Horizontal arrangement of composables Stack elements horizontally
LazyColumn Vertical scrollable list Efficient for large lists
LazyRow Horizontal scrollable list Efficient for horizontal lists

Example: Using Box for Overlays

import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun OverlayExample() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(
            text = "Background",
            modifier = Modifier.align(Alignment.Center)
        )
        Text(
            text = "Overlay",
            modifier = Modifier
                .align(Alignment.Center)
                .offset(y = 40.dp)
        )
    }
}

Example: Nested Layouts

import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp

@Composable
fun NestedLayouts() {
    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .height(60.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(text = "Top Bar")
        }
        Spacer(modifier = Modifier.height(16.dp))
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
        ) {
            Text(text = "Main Content")
        }
    }
}

4. Managing State

Example: Basic State with mutableStateOf

import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.unit.dp

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Text(text = "Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}
Property Description Usage
mutableStateOf(T) Creates a mutable state holder for a value of type T var count by remember { mutableStateOf(0) }
derivedStateOf { ... } Creates a state that is derived from other states val isEven = derivedStateOf { count % 2 == 0 }
remember { ... } Stores state across recompositions remember { mutableStateOf(0) }
Note: mutableStateOf is used to create a state variable that will trigger recomposition when changed.

Example: State with ViewModel

import androidx.lifecycle.ViewModel
import androidx.compose.runtime.*

class CounterViewModel : ViewModel() {
    private val _count = mutableStateOf(0)
    val count: Int
        get() = _count.value

    fun increment() {
        _count.value++
    }
}

@Composable
fun CounterWithViewModel(viewModel: CounterViewModel) {
    val count = viewModel.count

    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Text(text = "Count: $count")
        Button(onClick = { viewModel.increment() }) {
            Text("Increment")
        }
    }
}

State Management Best Practices

  • Use remember to preserve state across recompositions
  • Prefer mutableStateOf for simple local state
  • Use derivedStateOf to compute derived values efficiently
  • For complex state, consider using ViewModel with LiveData or StateFlow

5. Using Modifiers

Modifiers in Jetpack Compose are used to customize the appearance and behavior of composables. They allow you to apply transformations like padding, margins, size constraints, and layout adjustments.

Modifier Description Usage Example
padding() Applies padding around the composable
Box(
    modifier = Modifier.padding(16.dp)
)
size() Sets the width and height of the composable
Image(
    modifier = Modifier.size(100.dp),
    painter = painterResource(id = R.drawable.icon),
    contentDescription = null
)
background() Sets the background color or drawable
Text(
    text = "Hello",
    modifier = Modifier.background(Color.Blue)
)
border() Adds a border around the composable
Box(
    modifier = Modifier.border(2.dp, Color.Black)
)
clickable() Makes the composable respond to clicks
Text(
    text = "Tap me",
    modifier = Modifier.clickable { /* Handle click */ }
)

Example: Combining Modifiers

Box(
    modifier = Modifier
        .size(100.dp)
        .padding(16.dp)
        .background(Color.Red)
        .border(2.dp, Color.Black)
)

6. Handling User Input

Component Description Usage
TextField Displays a text input field for user entry. TextField(value = text, onValueChange = { text = it })
Button Triggers an action when clicked. Button(onClick = { /* action */ }) { Text("Click") }
Switch Allows toggling between two states. Switch(checked = isSwitched, onCheckedChange = { isSwitched = it })

Example: Handling Text Input

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*

@Composable
fun TextInputExample() {
    var text by remember { mutableStateOf("") }

    Column(modifier = Modifier.padding(16.dp)) {
        TextField(
            value = text,
            onValueChange = { text = it },
            label = { Text("Enter text") }
        )
    }
}

Example: Handling Button Clicks

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*

@Composable
fun ButtonClickExample() {
    var counter by remember { mutableStateOf(0) }

    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = "Counter: $counter")
        Button(onClick = { counter++ }) {
            Text("Increment")
        }
    }
}

7. Using LazyColumn and LazyRow

LazyColumn and LazyRow are composables that efficiently render large lists of items by only creating and laying out the items currently visible on screen. They are essential for building performant UIs in Jetpack Compose.

Property Description Values
items List of items to render Items in a list or array
itemContent Composable to render each item Composable function
horizontalAlignment Horizontal alignment of items in LazyRow Start, CenterHorizontally, End
verticalAlignment Vertical alignment of items in LazyColumn Top, CenterVertically, Bottom

Example: Basic LazyColumn

val items = (1..100).map { "Item $it" }

LazyColumn {
    items(items) { item ->
        Text(text = item, modifier = Modifier.padding(16.dp))
    }
}

Example: LazyRow with Horizontal Layout

val items = (1..20).map { "Card $it" }

LazyRow {
    items(items) { item ->
        Card(
            modifier = Modifier
                .padding(8.dp)
                .size(100.dp),
            onClick = { /* Handle click */ }
        ) {
            Text(
                text = item,
                modifier = Modifier
                    .fillMaxSize()
                    .align(Alignment.Center)
            )
        }
    }
}
Note: LazyColumn and LazyRow use items to efficiently manage item rendering. For more complex lists, consider using itemsIndexed or item for custom item types.

Key Points

  • LazyColumn is used for vertical scrolling lists.
  • LazyRow is used for horizontal scrolling lists.
  • They only compose visible items, improving performance.
  • Use items or itemsIndexed to define the list content.

8. Implementing Navigation

Example: Basic Navigation Setup

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

@Composable
fun NavigationExample() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(navController)
        }
        composable("details") {
            DetailsScreen()
        }
    }
}

@Composable
fun HomeScreen(navController: NavHostController) {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text(text = "Home Screen")
    }
}

@Composable
fun DetailsScreen() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text(text = "Details Screen")
    }
}
Navigation Component Description Usage
NavHost Container for navigation graph NavHost(navController = navController, startDestination = "home")
composable Defines a screen in the navigation graph composable("home") { HomeScreen() }
NavController Manages navigation state and actions val navController = rememberNavController()
  1. Define navigation graph using NavHost
  2. Create composables for each screen
  3. Use composable to register screens in the graph
  4. Use navController to navigate between screens
  • Jetpack Navigation: Declarative, type-safe, built-in
  • Activity/Fragment: Imperative, more boilerplate, less safe

9. Applying Theming

Property Values Description
colorScheme ColorScheme Defines the color palette for the app
typography Typography Defines text styles and fonts
shapes Shapes Defines corner radii for components

Example: Defining a Custom Theme

val MyTheme = androidx.compose.material3.MaterialTheme(
    colorScheme = lightColorScheme(),
    typography = Typography(),
    shapes = Shapes()
)

Example: Using Theme in UI

@Composable
fun MyScreen() {
    MyTheme {
        Surface(
            color = MaterialTheme.colorScheme.background
        ) {
            Text(
                text = "Hello, World!",
                style = MaterialTheme.typography.body1
            )
        }
    }
}
Note: Theme properties can be customized to match your app's branding and design system.

10. Using Side Effects

Side effects in Jetpack Compose refer to operations that have observable effects outside the function's scope, such as I/O operations, network requests, or state mutations. Compose provides several mechanisms to handle side effects safely within the composable function lifecycle.

Effect Type Description Usage Example
LaunchedEffect Launches a coroutine inside a composable and cancels it when the composable is disposed.
LaunchedEffect(key1) {
    // Perform async operation
    viewModel.fetchData()
}
DisposableEffect Runs a block of code when the composable is first composed and disposes of resources when it's removed.
DisposableEffect(scope) {
    val listener = OnBackPressedCallback {
        // Handle back press
    }
    backHandler.addCallback(listener)
    onDispose {
        backHandler.removeCallback(listener)
    }
}
RecompositionEffect Triggers recomposition when a specific condition changes.
RecompositionEffect(key1) {
    // Triggers recomposition when key1 changes
    Log.d("Compose", "Recomposing with $key1")
}

Example: Using LaunchedEffect to Fetch Data

import androidx.compose.runtime.*

@Composable
fun DataScreen(viewModel: MyViewModel) {
    val data by viewModel.data.observeAsState()

    LaunchedEffect(Unit) {
        viewModel.fetchData()
    }

    data?.let { 
        Text(text = it.toString())
    }
}
Note: LaunchedEffect is ideal for starting background tasks or observing LiveData/StateFlow.
Warning: Avoid using LaunchedEffect inside loops or conditions that might change frequently, as it can lead to excessive recompositions.

Best Practices for Side Effects

  • Use LaunchedEffect for asynchronous operations like API calls.
  • Use DisposableEffect for managing lifecycle-dependent resources like back press handlers.
  • Use RecompositionEffect to react to changes in values that require recomposition.

11. Implementing Animation

Example: Basic Fade Animation

import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun FadeAnimation() {
    var visible by remember { mutableStateOf(true) }

    val alpha by animateFloatAsState(
        targetValue = if (visible) 1f else 0f,
        animationSpec = tween(durationMillis = 300)
    )

    Box(
        modifier = Modifier
            .size(100.dp)
            .alpha(alpha)
    ) {
        Canvas(modifier = Modifier.fillMaxSize()) {
            drawCircle(Color.Blue)
        }
    }

    Text(
        text = "Click to fade",
        modifier = Modifier
            .padding(16.dp)
            .clickable { visible = !visible }
    )
}
Property Values Description
animateFloatAsState targetValue: Float Animates a float value over time
animateIntAsState targetValue: Int Animates an integer value over time
animateDpAsState targetValue: Dp Animates a dp value over time
tween durationMillis: Int = 300 Easing function for smooth transitions

Example: Size Change Animation

import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun SizeAnimation() {
    var size by remember { mutableStateOf(50.dp) }

    val animatedSize by animateDpAsState(
        targetValue = if (size == 50.dp) 100.dp else 50.dp,
        animationSpec = tween(durationMillis = 500, easing = LinearEasing)
    )

    Box(
        modifier = Modifier
            .size(animatedSize)
            .background(Color.Green)
    )

    Button(
        onClick = { size = if (size == 50.dp) 100.dp else 50.dp },
        modifier = Modifier.padding(16.dp)
    ) {
        Text(text = "Animate Size")
    }
}

Example: Rotation Animation

import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp

@Composable
fun RotationAnimation() {
    var rotation by remember { mutableStateOf(0f) }

    val animatedRotation by animateFloatAsState(
        targetValue = rotation,
        animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing)
    )

    Canvas(
        modifier = Modifier
            .size(100.dp)
            .graphicsLayer {
                rotationZ = animatedRotation
            }
    ) {
        drawCircle(Color.Red)
    }

    Text(
        text = "Rotate",
        modifier = Modifier
            .padding(16.dp)
            .clickable { rotation += 45f }
    )
}

12. Interoperating with Views

Jetpack Compose provides mechanisms to integrate with existing View-based UIs, allowing developers to gradually migrate or combine both approaches in the same app.

Approach Description Usage
View Composables Wraps View-based components using AndroidView.
AndroidView(
    factory = { context -> Button(context) }
)
Compose Views Uses ComposeView to embed Compose UI inside View hierarchies.
val composeView = ComposeView(context)
composeView.setContent { Text("Hello") }

Example: Using AndroidView

class MyActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Column {
                Text("Compose UI")
                AndroidView(factory = { context ->
                    Button(context).apply {
                        text = "Native View"
                    }
                })
            }
        }
    }
}
Note: AndroidView is useful for integrating legacy UI components or third-party View libraries.
Warning: Performance can degrade if too many View components are used in a Compose layout.

Was this article helpful?