
Designing Slot APIs in Jetpack Compose | by Anton Popov | Dec, 2022 | Sprite Tech
Limiting the content material of the composable lambda utilizing layoutId
modifier.
Jetpack Compose launched us to a brand new idea — Slot API. It empowers builders to create versatile but easy-to-use reusable UI elements. Nonetheless, typically there may be an excessive amount of flexibility — we’d like a strategy to enable solely a sure variety of UI elements to be positioned right into a slot.
However easy methods to do it? As we speak we’ll discover out. Strap in!
Think about we’re designing our personal TopAppBar
:
@Composable
enjoyable TopAppBar(
title: String,
icon: @Composable () -> Unit,
)
And we have already got a customized Icon
:
@Composable
enjoyable Icon(painter: Painter, tint: Coloration = DefaultTintColor)
However we wish customers of TopAppBar
to have the ability to place one and just one Icon
composable into an icon
slot.
The best approach is to simply do that:
@Composable
enjoyable TopAppBar(
title: String,
icon: painter: Painter,
iconTint: Coloration = DefaultTintColor,
)
// ...
Icon(painter, iconTint)
// ...
Nonetheless, if an Icon
element has many parameters (5–9 or much more), and/or TopAppBar
has many icons, this answer turns into impractical.
We are able to create a TopAppBarIcon
knowledge class particularly for TopAppBar
:
knowledge class TopAppBarIcon(
val painter: Painter,
val tint: Coloration = DefaultTintColor,
)@Composable
enjoyable TopAppBar(
title: String,
icon: TopAppBarIcon,
)
// ...
Icon(icon.painter, icon.tint)
// ...
Nonetheless, this answer has many disadvantages:
- Code duplication. A listing of
Icon
‘s parameters and their default values are duplicated inTopAppBarIcon
, which will likely be a headache to take care of. - Combinatorial explosion. If an icon will likely be utilized in numerous different elements, there will likely be numerous wrapper lessons for a similar
Icon
element. - Not idiomatic. Jetpack Compose closely makes use of Slot APIs, and builders are used to it. This strategy strays away from the conventions and confuses devs.
- Recomposition scope. If
icon.tint
adjustments, it would set off a recomposition of the entireTopAppBar
, which isn’t very environment friendly, particularly when utilizing animations (animating tint, for instance).
Compose Format subsystem has a factor referred to as layoutId
— a parameter that each LayoutNode can have (carried out utilizing ParentDataModifier).
First, it’s set utilizing a Modifier.layoutId
, then — learn in a structure (measuring) section.
Making use of this information to our drawback, firstly we use Modifier.layoutId
inside an Icon
like this:
@Composable
enjoyable Icon(painter: Painter, tint: Coloration = 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,
) = Format(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 really measure or structure a toddler
structure(0, 0)
This operate is a customized structure that doesn’t really measure or structure any youngsters, it’s simply checking if a single allowed youngster has a required layoutId.
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 here are some take a look at instances:
@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 should utilize any composable features that don't emit UI
keep in mind "One thing"
LaunchedEffect(Unit) delay(200)
Icon(painter = rememberVectorPainter(Icons.Default.Add))
// ✅
TopAppBar(title = "Lorem")
IconWrapper()
If you would like much more granular management over what Icon
s could be handed to TopAppBar
, you’ll be able to create a composable wrapper that can solely enable a sure subset of all attainable Icon
configurations:
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()
// ...
Utilization:
@Preview
@Composable
enjoyable TestCases() = Column
// ✅
TopAppBar(title = "Lorem")
TopAppBarIcon(painter = rememberVectorPainter(Icons.Default.Add))
Due to TopAppBarScope
we even get a pleasant autocompletion:
TopAppBarScope
In fact, this strategy can simply be prolonged to simply accept a outlined variety of totally different UI elements.
That’s all for right now, I hope it helps! Be at liberty to go away a remark if one thing will not be clear or you probably have questions. Thanks for studying!
– Designing Slot APIs in Jetpack Compose | by Anton Popov | Dec, 2022