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.