Introduction to Jetpack Compose Modifiers

Scope contained modifiers, recombined modifiers, interaction modifiers, and more

Lucca Beurmann
Better Programming

--

Image of a chameleon laying on a tree
Photo by Christopher Ott on Unsplash

This article presumes the reader to have basic knowledge on how Jetpack Compose works, if you are not familiar with @Composable annotated functions, I advise you to study these subjects before reading this article.

Jetpack Compose is a library designed to bring declarative UI patterns to Android Development.

It’s a game-changing feature that completely changes the way developers have to think and design their UI, given that there’s little to do with the (old) XML view system.

The default Composables (based on the Material UI specification) are highly customizable by default, but the existence of Modifiers increases the customizability possibilities to more than nine thousand.

Customizing a regular component

Most Composables are customizable through their parameters, for example, we can change the color and the current progress of a CircularProgressBar() , or set leading/trailing icons for TextField() Composables.

But in most cases, this may not be enough. Boxes for example don’t have many customization options through their parameters.

Gist 1: Box Composable available parameters

The Box() Composable doesn’t look like a very flexible component, given that we don’t have explicit parameters to modify colors, shape, borders, height, width, and many other useful properties to achieve the UI goal.

But the thing is: we don’t need all of these parameters simply because of the existence of Modifiers.

What’s a Modifier?

According to Android’s documentation, a Modifier is a chained set of properties that can be assigned to a Composable, capable of mutating the layout, adding event listeners, and much more.

Image 1: Some of the modifiers available for Box Composable inside a Scaffold.

Modifiers are chained using a Factory pattern, so if we desire to set the box background color to yellow and set its height and width we could do something like this:

Gist 2: Colouring a Box and setting its dimensions
Image 2: Result of code designed in Gist 2.

Sometimes it’s a matter of order

When making minor changes to a Composable the order of the Modifiers may make no difference, but when a more complex layout is needed we must pay attention to the order of our chained Modifiers. To exemplify this usage we’ll code the layout below:

First, we are going to create the base structure of our layout, with a Scaffold as the screen “skeleton” and a box to wrap our content. Note that we are already using a modifier here Modifier.fillMaxSize() to make the Box Composable fit the whole size of the Scaffold.

Now let’s add our composables, modifiers, and understand what’s going on here:

To achieve this layout I decided to use a Box as a screen wrapper with the modifier fillMaxSize() , so this same composable will fill the whole size of its parent. Complementary to the box I used a Column to be our circle with the Text composables inside.

In this exact Modifier order, we can easily achieve the gray circle with two texts (one on top of the other), now we let’s understand what each of the Column’s modifiers does and then change their order to purposely cause the layout to break.

  • clip: The clip modifier serves to clip the Composable so we can easily change its shape. Using this modifier we can transform a rectangular Box (the default shape of the Box Composable) into a rounded corner rectangle or a circle for example.
  • fillMaxWidth: This modifier serves to inform the Composable to fit the parent’s max-width(this is similar to a 0dp width View constrained to the start and end of its parent when using Constraint Layout). The fraction parameter serves to define the “weight” of this autofill, for example, when using 0.1f we’ll fill 10% of the parent’s width and 1.0f will fill the whole available width.
  • aspectRatio: This modifier will try to equalize the height and width of a composable according to the ratio parameter. By using clip we defined our composable to be a Circle and fillMaxWidth states that our circle's diameter will fill 80% of the parent’s max-width, but this solution will not result in a perfect circle, by removing the aspectRatio we can see that the Column is shaped like a rounded cornered rectangle and not a circle. This happens because the circle needs the height and width to withhold the same value, so by using aspectRatio(1) we can ensure that our circle has the same width and height.
  • backgroundColor: Sets the background color of a composable to the given value. The backgroundColor is one of the modifiers in which order matters, and we can see this behavior if we move backgroundColor to before the clip modifier. This happens because essentially we are painting the composable before clipping its bounds, for practical matters, our Column is clipped to be a Circle. But because we painted it before the clipping it looks like the clip modifier isn’t working, but it is, it’s just a matter of order.

Scope contained Modifiers

Compose Standard library delivers tons of common scope modifiers, in other words, modifiers that are available by default, therefore independent of the current scope.

But whenever Composables like Column, Row, and Box are used you’ve probably noticed that the content lambda is wrapped inside a scope, for example, in columns we have ColumnScope :

ColumnScope is an interface responsible to set the height of a Composable relative to its content size(wrap-content by default), and also provides a limited set of modifiers only available inside of this scope: weight, align, and alignBy , these are what I call scope-retained Modifiers.

You can read more about scopes and their respective retained modifiers by accessing the source code through the IDE or by reading the official documentation.

If we go back to our Circle Shaped column layout and add the align modifier to our Text Composables we can align them horizontally in the column

These alignment modifiers are also available in the Row Composable, but in this case, they are responsible to align the content vertically inside the row. This is also true for the BoxScope, but in this case we have modifiers that can control the position both horizontally and vertically.

The changes above will produce the layout below, by adding the align modifiers to our Text composables we have direct control of their horizontal position inside the Column.

Recombining Modifiers

Modifiers can also be recombined through the creation of a single modifier that applies other modifiers, in our case, we can create a makeColouredCircle that receives a color parameter.

Since Modifiers are chained one after another we have to guarantee that the ones previously added won’t be lost when applying our customized modifier, that's why we have to use this.then statement. The then function serves to append more modifiers to a pre-existing Modifier instance.

In this example, we are adding measurement modifiers in our custom Modifier but keep in mind this IS NOT a good practice, usually fillMaxWidth and aspectRatio should be used directly in our Column Composable

You don’t have to worry about keeping this rule in mind every time you create a new a custom Modifier, if you try simply returning a brand new Modifier instance the IDE will show you an error:

Now we can use our custom Modifier in our Column and achieve the same result we had before. Note that now we can dynamically change the color of our Circle:

Interaction Modifiers

Modifiers can also be used to add interactions to our Composables, like making the clickable, draggable, and many more. To exemplify this possibility we’ll use a very common use case: a list of clickable cards which leads to a details page.

Good practice tip: Whenever you create a Composable define a Modifier parameter with a default empty Modifier. This way we can make our Composables more flexible.

This is our base layout:

Now we are going to add a callback to our Card Composable so we can define what happens when we click on it.

To make our composable clickable we must use the clickable modifier, this enables the component to receive click or accessibility click events.

Notice we are simply appending the onClick parameter to our composable’s Modifier, so why not remove this parameter and simply add the clickable property directly in the modifier parameter? Well, that’s a matter of readability and good practices. By explicitly defining the onClick parameter we can assert that this given composable is supposed to be clickable, without this parameter, there is no way for other developers to know about this expected click event in the Card Composable because theoretically, every Composable function that has a Modifier parameter is clickable if you append the clickable modifier to its modifier chain.

Now let’s add some behavior for our Card Composables.

Done, now the cards are clickable!

References

https://developer.android.com/jetpack/compose/modifiers

https://developer.android.com/jetpack/compose/modifiers-list

--

--

Mobile Software Developer, technology enthusiast, video game lover and a cat person.