Delegation Pattern To Write Reusable Compose Components

Ali Moghadam
5 min readSep 2, 2023

--

One of the challenges of software development is creating reusable components that can be easily integrated into a larger system. A component is a piece of code that performs a specific function and can be used by other parts of the system. For example, a component can be a button, a text, a check box, or an image. A system can be composed of many components that interact with each other to provide the desired functionality.

However, adding a new component to an existing system is not always straightforward. It may require modifying the code of the system or other components to accommodate the new component.

Therefore it’s expected to implement components in a way that they require the minimum of changes in the future when we want to reuse them.

In this article, I will discuss a solution to write compose functions that are more reusable.

Atomic design is a valuable technique for creating reusable compose components.

Atomic design is a methodology for creating and maintaining UI components that are consistent, reusable, and scalable. It is based on the idea of breaking down UI elements into five levels of abstraction: atoms, molecules, organisms, templates, and pages.

To harness its full potential, we need to follow the correct approach for implementing them.

In the following, I will show you a common mistake that developers may encounter when using Jetpack Compose.

🔴 This is the wrong way to create a custom text component in compose, because it does not have all of text’s attributes like color, modifier, fontFamily and etc.

// This is a wrong implementation for Compose components
@Composable
fun HeaderText(
text: String,
) {
Text(
text = text,
style = MaterialTheme.typography.headlineLarge,
maxLines = 1,
)
}


// This is an usage
@Composable
fun Usage() {
HeaderText(
text = "Products",
)
}

The main problem is that if we need to a color to HeaderText(), we must modify its attributes.

@Composable
fun HeaderText(
text: String,
color: Color,
) {
Text(
text = text,
style = MaterialTheme.typography.headlineLarge,
color = color,
maxLines = 1,
)
}

Here, you can see component changes.

Solution

With the delegation pattern, we can reduce the changes we need to make to the compose component.

Delegation is simply passing a duty off to someone/something else. Delegation can be an alternative to inheritance.

It means that use component’s properties in another component, and forward functionality to it.

Advantages:

  • Minimal changes to add a property to a compose component
  • Decouple compose components and promote reusability
  • Avoid writing less-used compose components

By using delegation, we can inherit all the behavior of HeaderText() from Text(). We do this by passing all the parameters of Text() to HeaderText(), and giving them default values.

Also, We can prevent to have some of its parameters like textAlign, softWrap, style and define them into the HeaderText() based on our needs.

We have completed the implementation of the HeaderText() component which uses The Delegation rule.

@NonRestartableComposable
@Composable
fun HeaderText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
) {
Text(
text = text,
modifier = modifier,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = TextAlign.Center,
lineHeight = lineHeight,
overflow = overflow,
softWrap = false,
maxLines = maxLines,
onTextLayout = onTextLayout,
style = MaterialTheme.typography.headlineLarge,
)
}

Don’t forget to add @NonRestartableComposable to the HeaderText().

It prevents the function from being skipped or restarted by the Compose runtime. This means that the function will always run when its parent recomposes, and it will not be affected by changes in its parameters or state.

Recomposition count based on @NonRestartableComposable:

HeaderText() With @NonRestartableComposable
HeaderText() Without @NonRestartableComposable

Let us go back to the first example and analyze it in more detail.

Without delegation 😥

With Delegation 😍

Using this pattern, developers can build a UI kit that contains many compose components that require minimal changes.

Conclusion

In this article, I have explained how to use delegation pattern and atomic design pattern to create reusable compose components in Jetpack Compose. By applying these techniques, we can avoid modifying the code of the components or the system when adding a new property to a component. We can also decouple components and promote their reusability. This way, we can improve the quality and maintainability of our software development. I hope you have found this article useful and learned something new.

Thank you for reading.

Ali Moghadam 💙

--

--