Skip to main content

Compose Multiplatform Composables

Overview

Composables are the building blocks of Compose Multiplatform (CMP) applications. They are functions that describe a part of your app's UI and can be reused across different platforms (Android, iOS, Desktop, Web).

Basic Composable Structure

Simple Composable

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

Composable with Parameters

@Composable
fun UserCard(
name: String,
email: String,
avatarUrl: String? = null,
onClick: () -> Unit = {}
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
if (avatarUrl != null) {
AsyncImage(
model = avatarUrl,
contentDescription = "Avatar of $name",
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
)
} else {
Box(
modifier = Modifier
.size(48.dp)
.background(Color.Gray, CircleShape),
contentAlignment = Alignment.Center
) {
Text(
text = name.first().uppercase(),
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}

Spacer(modifier = Modifier.width(12.dp))

// User info
Column {
Text(
text = name,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
Text(
text = email,
color = Color.Gray,
fontSize = 14.sp
)
}
}
}
}

State Management in Composables

Local State

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

Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}

Derived State

@Composable
fun TodoList(todos: List<Todo>) {
var searchQuery by remember { mutableStateOf("") }

val filteredTodos by remember(searchQuery, todos) {
derivedStateOf {
if (searchQuery.isEmpty()) {
todos
} else {
todos.filter { it.title.contains(searchQuery, ignoreCase = true) }
}
}
}

Column {
TextField(
value = searchQuery,
onValueChange = { searchQuery = it },
label = { Text("Search todos") }
)

LazyColumn {
items(filteredTodos) { todo ->
TodoItem(todo = todo)
}
}
}
}

Platform-Specific Composables

Conditional Compilation

@Composable
fun PlatformSpecificButton(
text: String,
onClick: () -> Unit
) {
Button(
onClick = onClick,
modifier = Modifier
.then(
// Platform-specific modifiers
when (Platform.current) {
Platform.Android -> Modifier.background(Color.Blue)
Platform.IOS -> Modifier.background(Color.Red)
Platform.Desktop -> Modifier.background(Color.Green)
Platform.Web -> Modifier.background(Color.Yellow)
}
)
) {
Text(text)
}
}

Platform-Specific Behavior

@Composable
fun AdaptiveLayout() {
val platform = Platform.current

when (platform) {
Platform.Android, Platform.IOS -> {
// Mobile layout
Column {
Header()
Content()
BottomNavigation()
}
}
Platform.Desktop -> {
// Desktop layout
Row {
Sidebar()
Column {
Header()
Content()
}
}
}
Platform.Web -> {
// Web layout
Column {
Header()
Row {
Sidebar()
Content()
}
}
}
}
}

Advanced Composable Patterns

Custom Modifiers

@Composable
fun CustomCard(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Card(
modifier = modifier
.shadow(
elevation = 8.dp,
shape = RoundedCornerShape(12.dp)
)
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(12.dp)
),
shape = RoundedCornerShape(12.dp)
) {
content()
}
}

Reusable Components with Slots

@Composable
fun DialogBox(
title: String,
onDismiss: () -> Unit,
confirmText: String = "OK",
dismissText: String = "Cancel",
onConfirm: () -> Unit = {},
content: @Composable () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = { content() },
confirmButton = {
TextButton(onClick = {
onConfirm()
onDismiss()
}) {
Text(confirmText)
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(dismissText)
}
}
)
}

State Hoisting

@Composable
fun ParentComponent() {
var text by remember { mutableStateOf("") }
var isVisible by remember { mutableStateOf(true) }

ChildComponent(
text = text,
isVisible = isVisible,
onTextChange = { text = it },
onVisibilityChange = { isVisible = it }
)
}

@Composable
fun ChildComponent(
text: String,
isVisible: Boolean,
onTextChange: (String) -> Unit,
onVisibilityChange: (Boolean) -> Unit
) {
Column {
TextField(
value = text,
onValueChange = onTextChange,
label = { Text("Enter text") }
)

Switch(
checked = isVisible,
onCheckedChange = onVisibilityChange
)

if (isVisible) {
Text("Current text: $text")
}
}
}

Performance Optimization

Remember and Derived State

@Composable
fun OptimizedList(items: List<ExpensiveItem>) {
val expensiveComputation by remember(items) {
derivedStateOf {
items.map { item ->
// Expensive computation
item.copy(processed = processItem(item))
}
}
}

LazyColumn {
items(expensiveComputation) { item ->
ItemRow(item = item)
}
}
}

Lazy Loading

@Composable
fun LazyImageGrid(images: List<ImageData>) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 128.dp),
contentPadding = PaddingValues(16.dp)
) {
items(images) { imageData ->
AsyncImage(
model = imageData.url,
contentDescription = imageData.description,
modifier = Modifier
.aspectRatio(1f)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
}
}
}

Common Patterns and Tricks

Loading States

@Composable
fun <T> AsyncContent(
state: AsyncState<T>,
onRetry: () -> Unit = {},
content: @Composable (T) -> Unit
) {
when (state) {
is AsyncState.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is AsyncState.Success -> {
content(state.data)
}
is AsyncState.Error -> {
ErrorView(
message = state.message,
onRetry = onRetry
)
}
}
}

Form Validation

@Composable
fun ValidatedTextField(
value: String,
onValueChange: (String) -> Unit,
label: String,
validation: (String) -> ValidationResult,
modifier: Modifier = Modifier
) {
var isDirty by remember { mutableStateOf(false) }
val validationResult = remember(value) { validation(value) }

Column {
TextField(
value = value,
onValueChange = {
onValueChange(it)
isDirty = true
},
label = { Text(label) },
isError = isDirty && !validationResult.isValid,
modifier = modifier
)

if (isDirty && !validationResult.isValid) {
Text(
text = validationResult.errorMessage,
color = Color.Red,
fontSize = 12.sp
)
}
}
}

data class ValidationResult(
val isValid: Boolean,
val errorMessage: String = ""
)

Theme-Aware Components

@Composable
fun ThemeAwareCard(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
val colorScheme = MaterialTheme.colorScheme

Card(
modifier = modifier,
colors = CardDefaults.cardColors(
containerColor = colorScheme.surface,
),
elevation = CardDefaults.cardElevation(
defaultElevation = 4.dp
)
) {
content()
}
}

Best Practices

1. Keep Composables Pure

// Good: Pure function
@Composable
fun UserProfile(user: User) {
Text("${user.name} (${user.email})")
}

// Bad: Side effects in composable
@Composable
fun UserProfile(user: User) {
LaunchedEffect(user.id) {
analytics.trackUserView(user.id) // Side effect
}
Text("${user.name} (${user.email})")
}

2. Use Default Parameters

@Composable
fun Button(
text: String,
onClick: () -> Unit,
enabled: Boolean = true,
modifier: Modifier = Modifier
) {
// Implementation
}

3. Compose for Reusability

@Composable
fun AppBar(
title: String,
actions: @Composable RowScope.() -> Unit = {},
navigationIcon: @Composable (() -> Unit)? = null
) {
TopAppBar(
title = { Text(title) },
actions = actions,
navigationIcon = navigationIcon
)
}

4. Handle Platform Differences Gracefully

@Composable
fun PlatformAwareComponent() {
val platform = Platform.current

when (platform) {
Platform.Android -> AndroidSpecificLayout()
Platform.IOS -> IOSSpecificLayout()
Platform.Desktop -> DesktopSpecificLayout()
Platform.Web -> WebSpecificLayout()
}
}

Testing Composables

Basic Testing

@Test
fun testGreeting() {
composeTestRule.setContent {
Greeting("World")
}

composeTestRule.onNodeWithText("Hello, World!").assertIsDisplayed()
}

Testing User Interactions

@Test
fun testCounterIncrement() {
composeTestRule.setContent {
Counter()
}

composeTestRule.onNodeWithText("Count: 0").assertIsDisplayed()
composeTestRule.onNodeWithText("Increment").performClick()
composeTestRule.onNodeWithText("Count: 1").assertIsDisplayed()
}

This comprehensive guide covers the essential aspects of Compose Multiplatform Composables, from basic usage to advanced patterns and best practices for cross-platform development.