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 background
, padding
, clip
, etc. and there are three very important convention/features that we need to keep in mind:
- Declare a Modifier default parameter in you Composable function to let caller decide how to place it if they want to custom it.
- Orders matter, each parameter in Modifier will effect the display sequentially.
- 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.
Default parameter
@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 Modifier
s? 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.
Order of Modifier
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
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 outer
, inner
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.
Scope of Modifier
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.