
Build your components the right way with Jetpack Compose
Jetpack Compose is Google’s modern toolkit for building native Android UI. Designed to simplify and accelerate UI development, it uses a declarative approach, allowing developers to directly define how the UI should look and respond to data changes. Unlike traditional XML-based layouts, Jetpack Compose lets you write UI code in Kotlin, which leads to more concise, readable, and maintainable code.
One of the standout features of Jetpack Compose is its composable functions, reusable components that manage their own state, handle UI logic, and can be combined to create complex layouts.
When building components, think about where and how they might be reused. Create generic composables (like buttons, cards, and text fields) that aren’t tied to specific screen logic. Make them adaptable by allowing customizable parameters, such as colors, shapes, or actions, so they can serve multiple purposes across your app.
Here are some examples of bad composables.
data class Person(
val id: String,
val name: String,
val surname: String,
val age: Int,
val createdAt: Long,
val email: String,
val phone: String,
val photo: String,
val likesCount: Int
)
@Composable
fun PersonLikeCountCell(person: Person) {
Row {
Text(text = person.name)
Icon(imageVector = Icons.Default.Star, contentDescription = null)
Text(text = "${person.likesCount} likes")
}
}
Why is this composable function bad? Because we are passing the Person
object here, while we need name
and likesCount
of Person. This makes our composables less reusable. So if we will have another model and we want to use this function, we will not be able to do that. Let’s rewrite our function to make it more reusable
@Composable
fun PersonLikeCountCell(name: String, likesCount: Int) {
Row {
Text(text = name)
Icon(imageVector = Icons.Default.Star, contentDescription = null)
Text(text = "$likesCount likes")
}
}
Now our function takes two parameters: name
and likesCount
. This means that we can reuse it anywhere just passing these two parameters. Our function became independent,
You should remember that your composables shouldn’t rely on data models everywhere where this possible. This will also prevent component from unnecessary recompositions.
Another thing that many engineers do wrong, is putting state in wrong place. Here is an example
@Composable
fun UserDetailsPage() {
var bgColor by remember {
mutableStateOf(Color.Red)
}
Column(
modifier = Modifier.background(bgColor)
) {
Button(onClick = {
bgColor = Color.Blue
}) {
Text(text = "Change color")
}
}
}
What is wrong with this composable?
The main issue here is that when we change the color system will start the composition and layout phases for our Column
. So if you want only change the color of component instead of backround modifier we should use drawBehind
modifier.
@Composable
fun UserDetailsPage() {
var bgColor by remember {
mutableStateOf(Color.Red)
}
Column(
modifier = Modifier.drawBehind {
drawRect(bgColor)
}
) {
Button(onClick = {
bgColor = if (bgColor == Color.Red) Color.Blue else Color.Red
}) {
Text(text = "Change color")
}
}
}
Now our system will skip the composition and layout phases, and will go straight to the draw phase.
Conclusion
In summary, following best practices in Jetpack Compose helps create efficient, responsive, and maintainable Android apps. By managing state wisely, structuring composables effectively, and optimizing performance, developers can deliver a seamless user experience.
Feel free to follow me on Twitter and don’t hesitate to ask questions related to Jetpack Compose.
Twitter: https://twitter.com/a_rasul98
Also check out my other posts related to Jetpack Compose:
Thanks for reading and see you later!