Streamline your UI code
When we get down to it, software is just a whole lot of if statements…
Modifiers in Jetpack Compose are super powerful, and sometimes we only want to add a modifier if a certain condition is met, but this can quickly get out of hand when the conditions get complex. It can also become quite unreadable and hard for other developers to trace what is being modified and why.
Setting the scene
Take a look at this example, I have a simple scene that switches between dark mode and light mode and a torch turns on and off:
This scene is quite complex to construct with a different Modifier
set for each condition:
@Composable | |
fun ComplexModifierScene( | |
nighttime: Boolean, | |
torchon: Boolean, | |
modifier: Modifier = Modifier | |
) { | |
val boxModifier = if (nighttime && torchon) { | |
modifier | |
.clip(CircleShape) | |
.fillMaxHeight() | |
.aspectRatio(1f) | |
.background(TorchLight) | |
.shadow(elevation = 4.dp, shape = CircleShape) | |
} else if (!nighttime) { | |
modifier | |
.fillMaxHeight() | |
.aspectRatio(1f) | |
.background(Sunlight) | |
} else { | |
modifier | |
.fillMaxHeight() | |
.aspectRatio(1f) | |
.background(Night) | |
.blur(20.dp) | |
} | |
Box(boxModifier) { | |
Image(painterResource(R.drawable.outline_yard_24), "Scene", Modifier.fillMaxSize()) | |
} | |
} |
I can try and simplify these modifiers to reduce duplicated code:
@Composable | |
fun ComplexModifierScene( | |
nighttime: Boolean, | |
torchon: Boolean, | |
modifier: Modifier = Modifier | |
) { | |
val backgroundColor = if (nighttime && torchon) { | |
TorchLight | |
} else if (!nighttime) { | |
Sunlight | |
} else { | |
Night | |
} | |
val boxModifier = modifier.background(backgroundColor) | |
.fillMaxHeight() | |
.aspectRatio(1f) | |
val updatedModifier = if (nighttime && torchon) { | |
boxModifier | |
.clip(CircleShape) | |
.shadow(elevation = 4.dp, shape = CircleShape) | |
} else if (!nighttime) { | |
boxModifier | |
} else { | |
boxModifier | |
.blur(20.dp) | |
} | |
Box(updatedModifier) { | |
Image(painterResource(R.drawable.outline_yard_24), "Scene", Modifier.fillMaxSize()) | |
} | |
} |
But this is hard to read and understand what is applied when. And for some cases (nighttime == false
) we are just passing through the boxModifier
as is to satisfy the if
statement return value. Also, in order to simplify I have had to change the order of some of the modifiers which has resulted in the clip & background modifier not being applied in the correct order, meaning the background goes outside the clip area:
The original scene on the left, the ‘simplified’ but wrong scene on the right
Create a conditional modifier
What we can do, is construct an extension function that will check the condition and then apply the modifier depending on the result of the condition:
fun Modifier.conditional( | |
condition: Boolean, | |
ifTrue: Modifier.() -> Modifier, | |
ifFalse: (Modifier.() -> Modifier)? = null, | |
): Modifier { | |
return if (condition) { | |
then(ifTrue(Modifier)) | |
} else if (ifFalse != null) { | |
then(ifFalse(Modifier)) | |
} else { | |
this | |
} | |
} |
Job Offers
Making use of the Modifier
concatenation function then
and lambdas in the function signature we can easily chain modifiers one after another.
This can be improved for performance by using the inline
Kotlin modifier (thank-you for pointing this out Ricardo Carrapiço!):
inline fun Modifier.conditional( | |
condition: Boolean, | |
ifTrue: Modifier.() -> Modifier, | |
ifFalse: Modifier.() -> Modifier = { this }, | |
): Modifier = if (condition) { | |
then(ifTrue(Modifier)) | |
} else { | |
then(ifFalse(Modifier)) | |
} |
Adding this into the scene:
@Composable | |
fun ComplexModifierScene( | |
nighttime: Boolean, | |
torchon: Boolean, | |
modifier: Modifier = Modifier | |
) { | |
val backgroundColor = if (nighttime && torchon) { | |
TorchLight | |
} else if (!nighttime) { | |
Sunlight | |
} else { | |
Night | |
} | |
val boxModifier = modifier | |
.conditional(nighttime && torchon, { | |
clip(CircleShape) | |
}) | |
.fillMaxHeight() | |
.aspectRatio(1f) | |
.background(backgroundColor) | |
.conditional(nighttime && torchon, { | |
shadow(elevation = 4.dp, shape = CircleShape) | |
}) | |
.conditional(nighttime && !torchon, { | |
blur(20.dp) | |
}) | |
Box(boxModifier) { | |
Image(painterResource(R.drawable.outline_yard_24), "Scene", Modifier.fillMaxSize()) | |
} | |
} |
This is now much easier to read. To even further simplify the conditions, we can nest them.
@Composable | |
fun ComplexModifierScene( | |
nighttime: Boolean, | |
torchon: Boolean, | |
modifier: Modifier = Modifier | |
) { | |
val backgroundColor = if (nighttime && torchon) { | |
TorchLight | |
} else if (!nighttime) { | |
Sunlight | |
} else { | |
Night | |
} | |
val boxModifier = modifier | |
.conditional(nighttime && torchon, { | |
clip(CircleShape) | |
}) | |
.fillMaxHeight() | |
.aspectRatio(1f) | |
.background(backgroundColor) | |
.conditional(nighttime, { | |
conditional( | |
condition = torchon, | |
ifTrue = { | |
shadow(elevation = 4.dp, shape = CircleShape) | |
}, | |
ifFalse = { | |
blur(20.dp) | |
} | |
) | |
}) | |
Box(boxModifier) { | |
Image(painterResource(R.drawable.outline_yard_24), "Scene", Modifier.fillMaxSize()) | |
} | |
} |
This gives the exactly the same scene as the original scene as we can respect the modifier ordering.
Exactly the same as the original
Null conditional modifier
A common condition that you may want to check is if something is null or not, we can also create a nullConditional
modifier that takes in a type argument and passes that in to the ifNotNull
branch so it can be used by the modifier:
inline fun <T> Modifier.nullConditional( | |
argument: T?, | |
ifNotNull: Modifier.(T) -> Modifier, | |
ifNull: Modifier.() -> Modifier = { this }, | |
): Modifier { | |
return if (argument != null) { | |
then(ifNotNull(Modifier, argument)) | |
} else { | |
then(ifNull(Modifier)) | |
} | |
} |
This can be used just the same as the regular conditional modifier, the condition variable value is passed into the ifNotNull
lambda so it can be used in the modifier being applied. In this example, I am passing in a nullable colour that can be applied if present:
@Composable | |
fun ComplexModifierScene( | |
nighttime: Boolean, | |
torchon: Boolean, | |
backgroundColor: Color?, | |
modifier: Modifier = Modifier | |
) { | |
val boxModifier = modifier | |
.conditional(nighttime && torchon, ifTrue = { | |
clip(CircleShape) | |
}) | |
.fillMaxHeight() | |
.aspectRatio(1f) | |
.nullConditional(backgroundColor, { | |
background(it) | |
}) | |
.conditional(nighttime, { | |
conditional( | |
condition = torchon, | |
ifTrue = { | |
shadow(elevation = 4.dp, shape = CircleShape) | |
}, | |
ifFalse = { | |
blur(20.dp) | |
} | |
) | |
}) | |
Box(boxModifier) { | |
Image(painterResource(R.drawable.outline_yard_24), "Scene", Modifier.fillMaxSize()) | |
} | |
} |
If I pass in a grey colour and compare to the original:
The original code in this blog post was developed with help from John Ernest Ramos.
Take a look at the full code in this demo on Github:
This article was previously published on proandroiddev.com