๐ Hi! Today, Iโd like to tell you about how I optimized a workflow for developing custom UI components in Jetpack Compose.
The Good Old Constraint Layout
Before Compose, many developers used Constraint Layout to build most of their appโs UI. One advantage it has over Compose โ instant visualization of paddings between different elements of the UI.
Constraint Layout editor in Android Studio
When Compose came around, it introduced us to the idea of writing UI directly in code without any graphic tools. It was a big change, but it turned out amazingly goodโdevelopers becameย muchย more productive and Android Studioโs preview feature eliminated most of the disadvantages of this approach.
The Problem
However, there is a use case where visualization of dimensions is still needed in live preview, while you are building the UI: complex design system components.
What is the value ofย thatย padding??
Typically, the code for these things looks like this:
val startPadding = when (size) { Small -> if (icon) 4 else 6 Medium -> if (icon) 6 else 10 Large -> if (icon) 10 else 14 }.dp // endPadding, verticalPadding, iconPadding, etc.
It is very easy to get lost in the numbers, sizes, and paddings. After modifying the code above, you look at the composable preview in AS and wonder, have youย actuallyย matched the design spec, or there is a small error in one of theย dozensย of configurations ๐ฉ?
To solve this problem, I decided to write a small library that would have a capability similar to the Constraint Layout visual editorโsย blueprint mode.
The Blueprint
The Blueprint library provides a way to visualize dimensional information in your UI using a simple DSL-based definition:
- Just wrap your target UI in aย
Blueprint
ย composable - Mark children withย
Modifier.blueprintId(id: String)ย modifier
- Write the blueprint definition
Blueprint( blueprintBuilder = { widths { group { "item0".right lineTo "item1".left "item0" lineTo "item0" "item2" lineTo "item3" } } heights { group { "item0Icon" lineTo "item0Text" } group { "item0" lineTo "item0" } group(End) { "item3Icon".bottom lineTo "item3Text".top } } } ) { val items = remember { listOf("Songs", "Artists", "Playlists", "Settings") } NavigationBar { items.forEachIndexed { index, item -> NavigationBarItem( modifier = Modifier.blueprintId("item$index"), icon = { Icon(Modifier.blueprintId("item${index}Icon"), TODO()) }, label = { Text(Modifier.blueprintId("item${index}Text"), TODO()) }, selected = index == 0, onClick = { TODO() } ) } } }
This is the result:
Blueprint of a Material3 NavigationBar
Another example:
Blueprint of a Material3 Button
Job Offers
Lines in one group will be drawn at the sameย depth level.
The placing order of groups is the following:
first called -> placed closer to the composable.
In other words: going out from the center, like in an onion.
To produce a pretty blueprint, try to place in one group only
non-overlapping dimension lines. Place the overlapping ones in different groups.
Features
You can customize:
- Line and border strokes (width and color)
- Font size and color
- Arrow style (length, angle, round or square cap)
- Decimal precision of the dimensional values
Of course, Blueprint works in Android Studioโs Previewโจ!
Blueprint in Android Studioโs Preview
Also, you can disable all the overhead of this library in your release builds by either:
- Disabling blueprint rendering usingย
blueprintEnabledย property.
- Using theย
no-op
ย version of the library:
dependencies { debugImplementation("com.github.popovanton0.blueprint:blueprint:LATEST_VERSION") releaseImplementation("com.github.popovanton0.blueprint:blueprint-no-op:LATEST_VERSION") }
How it works
blueprintId
ย modifiers collectย LayoutCoordinates
ย of each target usingย OnGloballyPositionedModifier
ย interface.ย LayoutCoordinates
ย contains info about the absolute and relative position of the target, plus its size.
Then, the modifier puts allย LayoutCoordinates
ย in aย ModifierLocalย map โย ModifierLocalBlueprintMarkersย (alternative toย CompositionLocalย for modifier chains).
// from: https://github.com/popovanton0/Blueprint/blob/main/blueprint/src/main/java/com/popovanton0/blueprint/BlueprintId.kt public fun Modifier.blueprintId( id: String, sizeUnits: SizeUnits? = null ): Modifier = // ... BlueprintMarkerModifier(id, sizeUnits) private class BlueprintMarkerModifier( private val id: String, ) : ModifierLocalConsumer, OnGloballyPositionedModifier { private var markers: MutableMap<String, BlueprintMarker>? = null override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) = with(scope) { markers = ModifierLocalBlueprintMarkers.current } override fun onGloballyPositioned(coordinates: LayoutCoordinates) { val markers = markers ?: return if (coordinates.isAttached) { markers.remove(id) markers[id] = BlueprintMarker(coordinates) } else { markers.remove(id) } } // ... }
LayoutCoordinates
ย are updated on each position and size change, thus keeping theย ModifierLocalBlueprintMarkersย map up-to-date.
Then, inย Blueprint
ย composable,ย ModifierLocalBlueprintMarkersย is provided with a value, and the blueprint is drawn on top of the target composable.
// from: https://github.com/popovanton0/Blueprint/blob/main/blueprint/src/main/java/com/popovanton0/blueprint/Blueprint.kt#L155 val markers = remember { mutableStateMapOf<String, BlueprintMarker>() } var rootCoordinates by remember { mutableStateOf<LayoutCoordinates?>(null) } Box( modifier = modifier // applying paddint to account for long dimension lines // outside of the wrapped composable .run { if (applyPadding) padding(blueprint, groupSpace) else this } // in the actual code, this modifier is // reimplemented, as to not depend on experimental APIs .modifierLocalProvider(ModifierLocalBlueprintMarkers) { markers } .onGloballyPositioned { rootCoordinates = it } .drawWithContent { drawContent() val params = // ... drawBlueprint(params) // big function ๐ } ) { content() }
So all of the logic for calculating sizes, relative positions, distances, and dimensions is handled in theย draw phase.
Conclusion
This library helped me a lot during the development of complicated design system components, and I hope it will help you too.
Feel free to โญย Blueprint libraryย on GitHub:
GitHub – popovanton0/Blueprint: ๐ Visualize dimensions of your composables on a blueprint!
Thatโs all for today, I hope it helps! Feel free to leave a comment if something is not clear or if you have questions. Thank you for reading!
This article was previously published onย proandroiddev.com