Custom Composable Design Patterns - Reusable UI Components in Jetpack Compose
Building reusable Compose components requires understanding key design patterns that make your code flexible, maintainable, and composable. This guide covers the essential patterns every Jetpack Compose developer should know.
Slot API Pattern - Content Lambdas
The Slot API pattern allows parent composables to accept content as lambda parameters, giving maximum flexibility:
@Composable
fun MyCard(
modifier: Modifier = Modifier,
title: @Composable () -> Unit,
content: @Composable () -> Unit,
actions: @Composable RowScope.() -> Unit = {}
) {
Card(modifier = modifier) {
Column {
Box(modifier = Modifier.padding(16.dp)) {
title()
}
Divider()
Box(modifier = Modifier.padding(16.dp)) {
content()
}
Row(modifier = Modifier.padding(8.dp)) {
actions()
}
}
}
}
// Usage
MyCard(
title = { Text("Title") },
content = { Text("Content here") },
actions = {
Button(onClick = {}) { Text("OK") }
}
)
Modifier Design - Always Accept Modifier Parameter
Every composable should accept a Modifier parameter as the first optional parameter. This allows callers to customize appearance and behavior:
@Composable
fun MyButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
colors: ButtonColors = ButtonDefaults.buttonColors()
) {
Button(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = colors
) {
Text(text)
}
}
// Callers can easily customize
MyButton(
text = "Click",
onClick = {},
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.height(48.dp)
)
State Holder Pattern with rememberXxxState()
Create reusable state holders with remember to encapsulate complex state logic:
@Stable
class DialogState(
val isVisible: MutableState<Boolean> = mutableStateOf(false),
val title: MutableState<String> = mutableStateOf("")
) {
fun show(title: String) {
this.title.value = title
isVisible.value = true
}
fun dismiss() {
isVisible.value = false
}
}
@Composable
fun rememberDialogState(): DialogState =
remember { DialogState() }
@Composable
fun MyScreen() {
val dialogState = rememberDialogState()
Button(onClick = { dialogState.show("Delete?") }) {
Text("Show Dialog")
}
if (dialogState.isVisible.value) {
AlertDialog(
onDismissRequest = { dialogState.dismiss() },
title = { Text(dialogState.title.value) }
)
}
}
CompositionLocal for App-Wide Values
Use CompositionLocal to provide values without passing them through every composable:
val LocalAppTheme = compositionLocalOf<AppTheme> {
error("AppTheme not provided")
}
@Composable
fun AppThemeProvider(
theme: AppTheme,
content: @Composable () -> Unit
) {
CompositionLocalProvider(LocalAppTheme provides theme) {
content()
}
}
@Composable
fun ThemedButton(text: String) {
val theme = LocalAppTheme.current
Button(
onClick = {},
colors = ButtonDefaults.buttonColors(
containerColor = theme.primaryColor
)
) {
Text(text)
}
}
Key Pattern Characteristics
| Pattern | Use Case | Benefit |
|---|---|---|
| Slot API | Flexible layouts | Maximum composition flexibility |
| Modifier Parameter | UI customization | Consistent customization API |
| State Holder | Complex state | Reusable, testable state logic |
| CompositionLocal | App-wide values | Reduce parameter drilling |
8 Android app templates available on Gumroad
Top comments (0)