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:

  1. Code duplication. A listing of Icon‘s parameters and their default values are duplicated in TopAppBarIcon, which will likely be a headache to take care of.
  2. 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.
  3. 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.
  4. Recomposition scope. If icon.tint adjustments, it would set off a recomposition of the entire TopAppBar, 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 Icons 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:

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

x