Harmonizing Modifiers in Jetpack Compose: A Tale of Flexibility and Usability | by Mahmoud Afarideh | Oct, 2023
In a previous article titled “Avoid Applying Modifiers to the Provided Modifier,” I highlighted the challenges we faced in the early days of our design system. The primary issue was the overuse of modifiers in our composable functions. For instance, a simple “primary button” composable would receive a Modifier as a parameter. However, every developer had a unique vision for the button’s appearance. Some wanted it taller, others preferred it in blue, and this endless cycle of modifications made it incredibly challenging to manage and customize our UI components effectively. 🧩
Fast forward to the present day, and our design system is much more robust. However, I continued to champion my belief that some modifier modifications can be helpful, especially when it comes to enhancing usability. One of my colleagues, Amin, shared a different perspective. He argued that not all modifier modifications are troublesome. In fact, some, like those related to click behavior, can enhance usability and reduce complexity. 🧐
However, adhering to the principle of “Avoid Applying Modifiers to the Provided Modifier” doesn’t mean that everything should be defined within the caller composable. The input modifier is meant to influence the layout of the component, and component-specific details should be determined within the component itself.
For example, to display a button on the screen, it might be presented with some padding or in a specific location, such as at the bottom of a box. These layout-related tasks are the responsibilities of the parent composable.
Conversely, the radius of the button, its color, or the text it displays are part of the button component’s specific functionalities. This doesn’t mean delegating everything to the component. 📦
@Composable
fun SimpleButton(
text: String,
type: ButtonType,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
BaseButton(
text = text,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(type.backgroundColor)
.clickable { onClick() },
)
}
}enum class ButtonType {
Primary,
Danger,
Neutral,
Success
}
As you see, I’ve defined some parameters to modify the button design and behavior, but the SimpleButton component is responsible for applying the modifier based on its specific requirements. This approach enhances the component’s clarity and reusability for developers. It eliminates the need to repeatedly consider both parent and component modifiers every time the button is used, saving valuable development time. Additionally, it fosters a common UI language between designers and developers, promoting efficient collaboration and streamlined workflows.
This shift in perspective didn’t introduce a revolutionary concept but clarified existing principles. For example, consider the “SimpleButton” composable, which now allows click functionality to be directly applied to the provided modifier. This approach enhances code clarity, eliminates ambiguity for developers, and ultimately enhances the usability of our UI components. 🛠️
@Composable
fun SimpleButton(
text: String,
type: ButtonType,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
BaseButton(
text = text,
modifier = modifier
.clip(RoundedCornerShape(8.dp))
.background(type.backgroundColor)
.clickable { onClick() }
)
}
I could accept just a single modifier and everyone calling SimpleButton needed to apply clickable modifier on the passing modifier.
Why the “onClick” Parameter Shines
💡 Clear and Specific: The “onClick” parameter unambiguously defines the composable’s purpose, sparing developers from delving deeply into the modifier.
💡 Reduced Complexity: Separating the click functionality from the modifier streamlines development, as developers no longer need to recall which modifier component handles click behavior.
💡 Enhanced Usability: By abstracting click behavior into a parameter, developers can confidently reuse the composable, knowing it will work consistently across various use cases.