Hi, I need suggestions on the best practices on Co...
# compose-android
f
Hi, I need suggestions on the best practices on Compose around when one view needs to update its padding based on the other view's measured height. More details are inside the thread. Thanks! 🙂
🧵 1
I need help in regards of Compose, previously when using XML+Code. I can have something like so:
Copy code
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="<http://schemas.android.com/apk/res/android>"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#FF9800"
        android:padding="8dp"
        android:text="Hello world"
        android:textColor="#FFFFFF"
        android:textSize="24sp" />

    <ListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>
and then on the code I can have something like so to calculate the height of the
text
and then applying the height to padding top of
list
.
Copy code
text.doOnLayout {
  if (list.paddingTop == 0) {
    list.updatePadding(top = it.measuredHeight)
  }
}
Here is something I come up with in Jetpack Compose:
Copy code
@Composable
fun SampleApp() {
    var searchBarHeight by remember { mutableStateOf(0.dp) }
    val density = LocalDensity.current
    val layoutDirection = LocalLayoutDirection.current

    Scaffold { paddingValues ->
        val finalPaddingValues = PaddingValues(
            start = paddingValues.calculateStartPadding(layoutDirection),
            top = paddingValues.calculateTopPadding() + searchBarHeight + 16.dp, // 16.dp because we apply padding 8.0 into DockedSearchBar
            end = paddingValues.calculateEndPadding(layoutDirection),
            bottom = paddingValues.calculateBottomPadding()
        )

        Box(
            modifier = Modifier
                .fillMaxSize()
        ) {
            DockedSearchBar(
                ... // irrelevant properties
                modifier = Modifier
                    .padding(paddingValues)
                    .fillMaxWidth()
                    .padding(8.dp)
                    .onSizeChanged { searchBarHeight = with(density) { it.height.toDp() } }
            ) {

            }
            LazyColumn(
                contentPadding = finalPaddingValues,
                modifier = Modifier
                    .fillMaxSize()
            ) {
                ... // irrelevant
            }
        }
    }
}
I was wondering if this is the best practice do this? Do you have any suggestions to improve? Thanks! 🙂
z
With this code, every time the search bar changes size, you’re going to recompose just to remeasure the padding.
PaddingValues
is an interface. You can create a single remembered instance of it that performs your calculations in the functions that actually return the individual values. Then you can skip the recomposition since only the padding modifier’s layout will be invalidated.
🙏 1
f
Wasn't that expected? Since we cannot avoid recomposition when we change the padding of a Composable? Let's say I replace
DockedSearchBar
with
Surface
and provide a button to change the height of
Surface
on button press, it will recompose
LazyColumn
since we cannot avoid recomposition because of padding changes. The
Surface
will also be recomposed because the height property changes on
Modifier
. 🤔
z
You absolutely can avoid recomposing when padding changes, as I explained in my previous message. When you’re just handling layout changes, you can almost always avoid recomposing.
f
Sorry I got curious on your message and tried to do what you meant. This is what I came up with:
Copy code
@Composable
fun SampleApp() {
    var surfaceHeight by remember { mutableStateOf(64.dp) }
    val contentItems = List(1000) { "Item-$it" }

    Scaffold { paddingValues ->
        val lazyFinalPaddingValues = remember {
            LazyPaddingValues(
                base = { paddingValues },
                surfaceBarHeightProvider = { surfaceHeight }
            )
        }
        Box(
            modifier = Modifier
                .fillMaxSize()
        ) {
            LazyColumn(
                contentPadding = lazyFinalPaddingValues,
                modifier = Modifier
                    .fillMaxSize()
            ) {
                items(contentItems) { item ->
                    Text(
                        item,
                        modifier = Modifier
                            .clickable { }
                            .fillMaxWidth()
                            .height(48.dp)
                            .padding(horizontal = 12.dp)
                            .wrapContentHeight()
                    )
                }
            }
            Surface(
                color = MaterialTheme.colorScheme.primary,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(surfaceHeight + paddingValues.calculateTopPadding())
            ) {
                Text(
                    "Sample app",
                    color = MaterialTheme.colorScheme.onPrimary,
                    modifier = Modifier
                        .padding(top = paddingValues.calculateTopPadding())
                        .fillMaxSize()
                        .padding(horizontal = 12.dp)
                        .wrapContentHeight()
                )
            }
            IconButton(
                onClick = {
                    surfaceHeight = (36..102).random().dp
                },
                colors = IconButtonDefaults.filledIconButtonColors(),
                modifier = Modifier
                    .align(Alignment.BottomEnd)
                    .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom))
                    .padding(8.dp)
            ) {
                Icon(Icons.Rounded.Refresh, contentDescription = "Refresh")
            }
        }
    }
}

class LazyPaddingValues(
    private val base: () -> PaddingValues,
    private val surfaceBarHeightProvider: () -> Dp
) : PaddingValues {
    override fun calculateBottomPadding(): Dp = base().calculateBottomPadding()
    override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp =
        base().calculateLeftPadding(layoutDirection)

    override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp =
        base().calculateRightPadding(layoutDirection)

    override fun calculateTopPadding(): Dp =
        base().calculateBottomPadding() + surfaceBarHeightProvider()
}
but as I check the layout inspector, the
LazyColumn
still recompose when the
surfaceHeight
changes. 🤔 Is there something wrong that I did in the code?
Here is the recording of the layout inspector: Thanks! 🙂
z
probably because you’re still reading
surfaceHeight
in composition, on line 38
You’ll want something like this:
Copy code
Modifier
    .layout { measurable, constraints ->
        val childConstraints = constraints.copy(minHeight = surfaceHeight, maxHeight = surfaceHeight)
        val placeable = measurable.measure(childConstraints)
        layout(placeable.width, placeable.height) {
            placeable.place(0, 0)
        }
    }
f
Thanks! Seems that solves the problem. But now I have more questions regarding composition and recomposition. But I guess that is for other times. Thanks again. 🙇