Slot API Design in Jetpack Compose | by Anton Popov | December 2022
Limit the content material of the composable lambda utilizing layoutId
modifier
Jetpack Compose launched us to a brand new idea: Slots API. Permits builders to create versatile simple to make use of and reusable UI elements. Nevertheless, typically there’s an excessive amount of flexibility: we want a strategy to permit solely a sure variety of UI elements to be positioned in a slot.
However methods to do it? Immediately we are going to discover out. Buckle up!
Think about that we’re designing our personal TopAppBar
:
@Composable
enjoyable TopAppBar(
title: String,
icon: @Composable () -> Unit,
)
And we have already got a behavior Icon
:
@Composable
enjoyable Icon(painter: Painter, tint: Colour = DefaultTintColor)
However we would like customers of TopAppBar
to have the ability to place one and just one Icon
composable in a icon
slot.
The best manner is to only do that:
@Composable
enjoyable TopAppBar(
title: String,
icon: painter: Painter,
iconTint: Colour = DefaultTintColor,
) {
// ...
Icon(painter, iconTint)
// ...
}
Nevertheless, if a Icon
element has many parameters (5–9 or much more), and/or TopAppBar
has many icons, this resolution turns into impractical.
We will create a TopAppBarIcon
knowledge class particularly for TopAppBar
:
knowledge class TopAppBarIcon(
val painter: Painter,
val tint: Colour = DefaultTintColor,
)@Composable
enjoyable TopAppBar(
title: String,
icon: TopAppBarIcon,
) {
// ...
Icon(icon.painter, icon.tint)
// ...
}
Nevertheless, this resolution has many disadvantages:
- code duplication. A listing of
Icon
Parameters and their default values are duplicated inTopAppBarIcon
will probably be a ache to keep up. - combinatorial explosion. If an icon is for use in lots of different elements, there will probably be many wrapper lessons for it.
Icon
element. - not idiomatic. Jetpack Compose makes use of slot APIs loads and the builders are used to it. This strategy strays from conference and confuses builders.
- Recomposition scope. Sure
icon.tint
modifications, it would set off a recomposition of the setTopAppBar
which isn’t very environment friendly, particularly when utilizing animations (animating tint, for instance).
The Compose Structure subsystem has a factor known as layoutId
— a parameter that every LayoutNode can have (applied utilizing main data modifier).
First, it’s configured utilizing a Modifier.layoutId
then – learn in a design section (measuring).
Making use of this data to our downside, we first use Modifier.layoutId
inside a Icon
like this:
@Composable
enjoyable Icon(painter: Painter, tint: Colour = DefaultTintColor) {
Field(Modifier.layoutId(IconLayoutId)) {
Icon(
painter = painter,
tint = tint,
contentDescription = null
)
}
}non-public object IconLayoutId
Then create a composable operate. RequireLayoutId
:
@Composable
enjoyable RequireLayoutId(
layoutId: Any?,
errorMessage: String = "Failed requirement.",
content material: @Composable () -> Unit,
) = Structure(content material) { measurables, constraints ->
val youngster = measurables.singleOrNull()
?: error("Solely a single youngster is allowed, was: ${measurables.measurement}")// learn layoutId of a single youngster
require(youngster.layoutId == layoutId) { errorMessage }
// don't truly measure or structure a toddler
structure(0, 0) {}
}
This operate is a customized structure that does not truly measurement or structure any youngsters, it solely checks if a single allowed youngster has a required structure ID.
Lastly, we use the operate like this:
@Composable
enjoyable TopAppBar(
title: String,
icon: @Composable () -> Unit,
) {
RequireLayoutId(
layoutId = IconLayoutId,
errorMessage = "Solely Icon is allowed",
content material = icon
)// later in code
icon()
}
Listed below are some check circumstances:
@Preview
@Composable
enjoyable TestCases() = Column {
// ✅
TopAppBar(title = "Lorem") {
Icon(painter = rememberVectorPainter(Icons.Default.Add))
}// ❌
TopAppBar(title = "Lorem") {
Button(onClick = {})
}
// ❌
TopAppBar(title = "Lorem") {
}
// ❌
TopAppBar(title = "Lorem") {
Field {
Icon(painter = rememberVectorPainter(Icons.Default.Add))
}
}
@Composable
enjoyable IconWrapper() {
// you need to use any composable features that don't emit UI
bear in mind { "One thing" }
LaunchedEffect(Unit) { delay(200) }
Icon(painter = rememberVectorPainter(Icons.Default.Add))
}
// ✅
TopAppBar(title = "Lorem") {
IconWrapper()
}
}
If you’d like much more granular management over what Icon
s may be handed to TopAppBar
you’ll be able to create a composable wrapper that may solely permit a sure subset of all the things attainable Icon
settings:
interface TopAppBarScope {
@Composable
enjoyable TopAppBarIcon(painter: Painter) {
Field(Modifier.layoutId(TopAppBarIconLayoutId)) {
Icon(painter = painter, tint = TopAppBarTint)
}
}companion object {
non-public val occasion = object : TopAppBarScope {}
inner operator enjoyable invoke() = occasion
}
}
non-public object TopAppBarIconLayoutId
@Composable
enjoyable TopAppBar(
title: String,
icon: @Composable TopAppBarScope.() -> Unit,
) {
// ...
RequireLayoutId(
layoutId = TopAppBarIconLayoutId,
errorMessage = "Solely TopAppBarIcon is allowed",
) {
TopAppBarScope().icon()
}
TopAppBarScope().icon()
// ...
}
Use:
@Preview
@Composable
enjoyable TestCases() = Column {
// ✅
TopAppBar(title = "Lorem") {
TopAppBarIcon(painter = rememberVectorPainter(Icons.Default.Add))
}
}
As a result of TopAppBarScope
we even get a pleasant autocompletion:
TopAppBarScope
After all, this strategy can simply be prolonged to just accept a particular variety of totally different UI elements.
That is all for right now, I hope it really works for you! Be happy to depart a remark if one thing is unclear or when you have questions. Thanks for studying!