DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Custom Composable Design Patterns - Reusable UI Components in Jetpack Compose

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") }
    }
)
Enter fullscreen mode Exit fullscreen mode

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)
)
Enter fullscreen mode Exit fullscreen mode

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) }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)