Blog Infos
Author
Published
Topics
,
Author
Published

As you might know, Jetpack Compose heavily leverage on Kotlin feature to implement Declarative UI pattern in Android. There are so many cool features that we probably couldn’t imagine how it work behind the scene when we learn it. Modifier would be one of the magical things worth to mention. Let’s dig deeper together to see how marvelous the design is today.

Modifier is a where we set some common attribute or appearance like backgroundpaddingclip, etc. and there are three very important convention/features that we need to keep in mind:

  1. Declare a Modifier default parameter in you Composable function to let caller decide how to place it if they want to custom it.
  2. Orders matter, each parameter in Modifier will effect the display sequentially.
  3. Scope matter as well, depend on which parent Composable you’re in. What Modifier you can use might be differ.

Let’s discuss all of these characteristics one by one.

@Composable
fun PaddedColumn(modifier: Modifier = Modifier) {
    Column(modifier.padding(10.dp)) {
        // ...
    }
}

You probably can found lot’s of sample code with a parameter like modifier: Modifier = Modifier. This give caller side the power to adjust the layout if they want to, but have you wondering why there is two Modifiers? In sort, the first Modifier is the type and second one is an instance, but let’s check the source code to better understand what’s going on:

interface Modifier {
  fun <R> foldIn(initial: R, operation: (R, Element) -> R): R
  fun <R> foldOut(initial: R, operation: (Element, R) -> R): R
  fun any(predicate: (Element) -> Boolean): Boolean
  fun all(predicate: (Element) -> Boolean): Boolean
  infix fun then(other: Modifier): Modifier =
    if (other === Modifier) this else CombinedModifier(this, other)

  companion object : Modifier {
    override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
    override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = initial
    override fun any(predicate: (Element) -> Boolean): Boolean = false
    override fun all(predicate: (Element) -> Boolean): Boolean = true
    override infix fun then(other: Modifier): Modifier = other
    override fun toString() = "Modifier"
    }
}

The answer is quite obviously, Modifier is an interface but there’s a companion object implement Modifier interface associate to it. So it can represent a type as well as an instance if need.

You can also open Android Studio and press shortcut cmd + B on each Modifier to go to the declaration of the object, and it will go to these two different places as well.

 

 

Another very important characteristic of Modifier is the order of each Modifier matters. As you can see on the image above, the red background will draw first before the yellow one and you can change it by just replace the order if you want.

Leverage on this characteristic we can have fully control of what we want to achieve. And it’s way more powerful than existing Android View system, for example there’s no margin needs anymore because we can achieve that by place the padding before background or after now.

As usual, that take a look at how this is achieved by Compose, let’s start from padding implementation:

fun Modifier.padding(all: Dp) =
    this.then(
        PaddingModifier(
            start = all,
            top = all,
            end = all,
            bottom = all,
            rtlAware = true,
            inspectorInfo = debugInspectorInfo {
                name = "padding"
                value = all
            }
        )
    )
infix fun then(other: Modifier): Modifier =
    if (other === Modifier) this else CombinedModifier(this, other)

 

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Kotlin variance modifiers and Covariant Object Nothing pattern.

Variance modifiers are a powerful feature, helping us in everyday programming, yet it is not understood by most developers. In this presentation, you will learn how it works, what are its limitations, and how it…
Watch Video

Kotlin variance modifiers and Covariant Object Nothing pattern.

Marcin Moskala
Android developer and Kotlin trainer certified
JetBrains

Kotlin variance modifiers and Covariant Object Nothing pattern.

Marcin Moskala
Android developer an ...
JetBrains

Kotlin variance modifiers and Covariant Object Nothing pattern.

Marcin Moskala
Android developer and Kot ...
JetBrains

Jobs

We found padding call then function with another PaddingModifier instance. And then function is calling CombinedModifier to combine them together:

class CombinedModifier(
    private val outer: Modifier,
    private val inner: Modifier
) : Modifier {
    override fun <R> foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
        inner.foldIn(outer.foldIn(initial, operation), operation)

    override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
        outer.foldOut(inner.foldOut(initial, operation), operation)

    override fun any(predicate: (Modifier.Element) -> Boolean): Boolean =
        outer.any(predicate) || inner.any(predicate)

    override fun all(predicate: (Modifier.Element) -> Boolean): Boolean =
        outer.all(predicate) && inner.all(predicate)

    override fun equals(other: Any?): Boolean =
        other is CombinedModifier && outer == other.outer && inner == other.inner

    override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode()

    override fun toString() = "[" + foldIn("") { acc, element ->
        if (acc.isEmpty()) element.toString() else "$acc, $element"
    } + "]"
}

The answer is quite clear, CombinedModifier keep the outerinner information and used in foldIn and foldOut with the correct order to keep it sequential when needed.

So if we have several common Modifier in child Composable that can extract to parent Composable, we should do it to avoid create necessary object create since it’s not a Builder pattern at all.

As an Android developer, we know that depend on what the parent View is, you will have different attribute set can be used in the xml layout like below example whereTextView can use layout_constraintXXX_toXXXOf to describe how to layout it:

<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Same scenario will happened in Compose and is way more tricky than Android View system as in Compose world everything can compose much dynamically. So how can we know what parent Composable will be in code level? Android team solve it by using Kotlin Extension function and Function literals with receiver gracefully. Let’s find out how together start from a simple example below:

Row {
    Text(
        text = "Test",
        modifier = Modifier.align(Alignment.CenterVertically)
    )
}
Column {
    Text(
        text = "Test",
        modifier = Modifier.align(Alignment.CenterHorizontally)
    )
}

Here we have two similar Text Composable inside Row and Column respectively. The only difference is the alignment, we want the Text center vertically in a Row and center horizontally in a Column as it looks better and also make sense semantically.

But can we swap CenterVertically and CenterHorizontally as it just some common parameter inside the align function, right? But once we try to do that, the IDE will show the error to us:

Type mismatch.
Required:
Alignment.Vertical
Found:
Alignment.Horizontal

Why the same align function need take different type? The answer is simple enough, they are not the same. If you go to the definition of each align function, you’ll see two version like below:

interface RowScope {
    fun Modifier.align(alignment: Alignment.Vertical): Modifier
}
interface ColumnScope {
    fun Modifier.align(alignment: Alignment.Horizontal): Modifier
}

Although they share the same name, but they are different given they belong to different interface and function signature. But how come we can access one of them in Row and Column? We have to go to the definition of Row and Column as well.

@Composable
inline fun Row(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    content: @Composable RowScope.() -> Unit
)
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
)

Yes, the lambda Composable function we passed to Row or Column will run on RowScope and ColumnScope interface respectively, and that’s why we will access different extension function in the end.

Feel interested? As Kotlin and Compose are both new and innovative, I’m pretty sure there are more interesting topics we can discover. If you haven’t try it, go ahead. It would be a wonderful journey I can promised you!!

Thanks to Omolara Adejuwon.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu