12 ways to improve your Jetpack Compose composable functions

by: | Jul 13, 2023

Jetpack Compose is a modern toolkit for building native Android user interfaces (UIs) with Kotlin programming language. It is a declarative UI framework that enables developers to create and manage UI components with less code and complexity.

When used correctly, Jetpack Compose can help Android developers create beautiful and responsive apps more easily and efficiently. However, that requires understanding how to define UI components using functions that build UI elements. These functions are called “composables” and they can be combined to create complex interfaces. Composables make it easy to create highly reusable UI components that can be shared across multiple screens and applications.

Here are 12 best practices to improve your composable functions and take your Android app project to the next level.

1. Add a modifier for layout composables

As the name suggests, a modifier is used to modify the composable. They are especially important for public components, allowing customization whenever it’s used in your app. Below is an example of what you should avoid and what you should consider doing.

DON’T DO THIS…

@Composable
fun MyComposable() {
  Column {
    ...
  }
}

INSTEAD, DO THIS…

@Composable
fun MyComposable(
  modifier: Modifier = Modifier 
) {
  Column(
    modifier = modifier
  ) {
    ...
  }
}

2. Include default parameters with modifiers

Public composables that have a modifier as a parameter should include the default value Modifier. It should appear as the first optional parameter after all the required parameters:

DON’T DO THIS…

@Composable
private fun MyComposable(
  modifier: Modifier,
  list: List<String>,
) {
  Column(modifier) {
    ...
  }
}

INSTEAD, DO THIS…

@Composable
private fun MyComposable(
  list: List<String>,
  modifier: Modifier = Modifier
) {
  Column(modifier) {
    ...
  }
}

3. Do not reuse modifiers

The Modifier from the parameter should be used by a single layout node in the composable function. If the provided modifier is used by multiple composables at different levels, unwanted behavior can happen as having the wrong customization of your composable function.

DON’T DO THIS…

@Composable
private fun MyComposable(modifier: Modifier = Modifier) {
  Column(modifier) {
    Text(modifier.clickable(), ...)
    Image(modifier.size(), ...)
    Button(modifier, ...)
  }
}

INSTEAD, DO THIS…

@Composable
private fun MyComposable(modifier: Modifier = Modifier) {
  Column(modifier) {
    Text(Modifier.clickable(), ...)
    Image(Modifier.size(), ...)
    Button(Modifier, ...)
  }
}

4. Inject ViewModel into the composable

For better testability, always inject the ViewModel into the composable as a parameter with a default value instead of creating it within the composable. This approach allows you to easily provide a mock ViewModel while testing, which in turn enables you to test your composable functions in isolation without relying on the actual implementation of the ViewModel.

DON’T DO THIS…

@Composable
fun MyComposable() {
  val viewModel by viewModel()
  // ...
}

INSTEAD, DO THIS…

@Composable
fun MyComposable(
  viewModel: MyViewModel = viewModel()
) {
  // ...
}

5. Do not forward the ViewModel

ViewModels have a longer lifetime than the composition because they survive configuration changes. Because of their long lifetime, ViewModel parameters should not hold long-lived references to state bound to the lifetime of the composition. If they do, it could cause memory leaks.
Instead, pass down the relevant data to the function and optional lambdas for callbacks.
In addition, previews don’t work with ViewModel, which makes it harder to preview your layout.

DON’T DO THIS…

@Composable
fun MyComposable(
   viewModel: MyViewModel = viewModel()
) {
  OtherComposable(viewModel)
}

INSTEAD, DO THIS…

@Composable
fun MyComposable(
  viewModel: MyViewModel = viewModel()
) {
  val uiState = viewModel.uiState.collectAsState()
  OtherComposable(
    data = uiState.data,
    onSaveButtonClicked = viewModel::onSaveButtonClicked
  )
}

6. Use remember state in composables

Make sure to use remember with a mutableStateOf (or any other state builders). Otherwise, a new state instance will be created when the function is recomposed, causing app performance issues such as slow screen rendering and inconsistent state.

DON’T DO THIS…

@Composable
fun MyComposable() {
  val count = 0
  Button(onClick = { count++ }) {
    Text("Increase Count")
  }

  Text(text = "Count: $count")
}

INSTEAD, DO THIS…

@Composable
fun MyComposable() {
val count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Increase Count")
}

Text(text = "Count: $count")
}

7. Do not emit multiple pieces of content

A composable function should emit either 0 or 1 piece of layout, but no more. A composable function should be cohesive and not rely on the function that is called.
The wrong example below shows that MyComposable is being called from a column, but this might not be true. MyComposable could also be called from a row, which would cause the wrong behavior.

Column {
  MyComposable()
}

DON’T DO THIS…

@Composable
private fun MyComposable() {
  Text(...)
  Image(...)
  Button(...)
}

INSTEAD, DO THIS…

@Composable
private fun MyComposable() {
  Column {
    Text(...)
    Image(...)
    Button(...)
  }
}

OR DO THIS…

@Composable
private fun ColumnScope.MyComposable() {
  Text(...)
  Image(...)
  Button(...)
}

8. Name CompositionLocals properly

CompositionLocals should be named by using the adjective Local as a prefix, followed by a descriptive noun that describes the value they hold. This makes it easier to know when a value comes from a CompositionLocal. Given that these are implicit dependencies, make them obvious.

DON’T DO THIS…

val ThemeLocal = compositionLocalOf { ... }

INSTEAD, DO THIS…

val LocalTheme = compositionLocalOf { ... }

9. Name multipreview annotations properly

With multipreview, you can define an annotation class that itself has multiple @Preview annotations with different configurations. Adding this annotation to a composable function will automatically render all the different previews at once.

Multipreview annotations should be named by using Previews as a suffix (or Preview if just one). These annotations have to be explicitly named to make sure that they are clearly identifiable as a @Preview alternative.

DON’T DO THIS…

@Preview(
  name = "small font",
  group = "font scales",
  fontScale = 0.5f
)
@Preview(
  name = "large font",
  group = "font scales",
  fontScale = 1.5f
)
annotation class PreviewsFontScale

INSTEAD, DO THIS…

@Preview(
  name = "small font",
  group = "font scales",
  fontScale = 0.5f
)
@Preview(
  name = "large font",
  group = "font scales",
  fontScale = 1.5f
)
annotation class FontScalePreviews

@FontScalePreviews
@Composable
private fun HelloWorldPreview() {
  Text("Hello World")
}

10. Name composable functions properly

Composable functions that return Unit should start with an uppercase letter. They are considered declarative entities that can be either present or absent in a composition — and, therefore, they follow the naming rules for classes.

However, composable functions that return a value should start with a lowercase letter instead. They should follow the standard Kotlin coding conventions for the naming of functions for any function annotated @Composable that returns a value other than Unit.

DON’T DO THIS…

@Composable
fun fancyButton(text: String, onClick: () -> Unit)

OR THIS…

@Composable
fun RenderFancyButton(text: String, onClick: () -> Unit)

OR THIS…

@Composable
fun drawProfileImage(image: ImageAsset)

OR THIS…

@Composable
fun Style(): Style

INSTEAD, DO THIS…

@Composable
fun FancyButton(text: String, onClick: () -> Unit)

OR DO THIS…

@Composable
fun BackButtonHandler(onBackPressed: () -> Unit)

OR DO THIS…

@Composable
fun defaultStyle(): Style

11. Order composable parameters properly

In Kotlin, it’s a good practice to write the mandatory parameters first, then the optional ones. This reduces the number of times you need to write the name of the arguments explicitly.

In addition, composables that have the Modifier as a parameter should name the parameter modifier and assign the parameter a default value of Modifier. It should appear as the first optional parameter in the parameter list.

DON’T DO THIS…

@Composable
fun ActionButton(
  modifier: Modifier = Modifier,
  text: String,
  onButtonClick: () -> Unit = {},
)

INSTEAD, DO THIS…

@Composable
fun ActionButton(
  text: String,
  modifier: Modifier = Modifier,
  onButtonClick: () -> Unit = {},
)

12. Make preview composables private

A @Preview composable function doesn’t need to have public visibility because it won’t be used in the UI. Making them private will prevent folks from using them unknowingly.

DON’T DO THIS…

@Composable
@Preview
fun ScreenPreview()

INSTEAD, DO THIS…

@Composable
@Preview
private fun ScreenPreview()

Need help with your Android app?

ArcTouch has been building lovable Android apps for companies of all sizes since the dawn of the app store. Learn more about ArcTouch’s Android development services and contact us for a free consultation.