Blog Infos
Author
Published
Topics
Published
๐Ÿ‘‹ 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.

A screenshot of android studio with constrained layout editor. Side-by-side are: blueprint and normal render windows. Constraint lines have numbers representing their length.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.

A button design component in many different parameter combinations

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:

  1. Just wrap your target UI in aย Blueprintย composable
  2. Mark children withย Modifier.blueprintId(id: String)ย modifier
  3. 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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Jetpack Compose: Drawing without pain and recomposition

This is a talk on recomposition in Jetpack Compose and the myths of too many calls it is followed by. I’ll briefly explain the reasons behind recompositions and why they are not as problematic as…
Watch Video

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jobs

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:

  1. Line and border strokes (width and color)
  2. Font size and color
  3. Arrow style (length, angle, round or square cap)
  4. 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:

  1. Disabling blueprint rendering usingย blueprintEnabledย property.
  2. 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

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Itโ€™s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu