Composable functions can react on changing state and as a result such functions can be re-invoked. This mechanism is a core concept of the Jetpack Compose approach for building UI. It's a powerful tool which allows us to implement, as part of the UI (item for list), whole screens and navigation between screens.
Jetpack Compose doesn't use the Android View framework under the hood. The framework uses its own approach of building UI which requires a set of Jetpack Compose libraries and a compiler plugin. In the end, all elements draw on the canvas and the approach doesn't use View Hierarchy. Under the hood, Jetpack Compose uses a data structure similar to "Gap Buffer" with additional modification; it is called "Slot Table". The result of the execution of each composable function stored in "Slot Table".
If you are not familiar with Jetpack Compose, you can check the "Jetpack Compose: Overview" article.
State of composable functions
Let's start with the additional possibilities of Composable functions:
- Composable function can be re-invoked at any time; it help us with recomposition;
- Composable function stores position in call graph (Positional Memoization); it helps us to avoid redrawing the whole UI only re-drawing a small part of the UI (favourite state for one of list items).
The main approach of building UI is the composition of composable functions. It helps us to redraw only the required pieces of the UI.
Having possibilities of Composable functions in mind we can explore the state of composable functions. Let's build a list of coffees which can be ordered in any amount.
When we split it into different components it's a "coffee information" and a "counter". Let's build it with Jetpack Compose. I recommend starting with a model of storing state.
@Model
data class CoffeeDrink(
val name: String,
@DrawableRes val imageRes: Int,
val description: String,
val price: Double,
var count: Int = 0
)
The @Model
is a very important annotation for Jetpack Compose because it makes that object trackable for changes and the composable function can be redraw after changing the object.
Afterwards, we can create a composable function for a counter.
@Composable
fun Counter(
coffeeDrink: CoffeeDrink,
onAddCoffeeDrink: (CoffeeDrink) -> Unit,
onRemoveCoffeeDrink: (CoffeeDrink) -> Unit
) {
Surface(
shape = RoundedCornerShape(size = 5.dp),
border = Border(1.dp, Color.Gray),
color = Color.Transparent
) {
Container(
modifier = LayoutHeight.Fill + LayoutWidth.Fill,
alignment = Alignment.Center
) {
Row {
Clickable {
Button(
modifier = LayoutWidth(40.dp) + LayoutHeight.Fill,
backgroundColor = Color.Transparent,
elevation = 0.dp,
onClick = { onRemoveCoffeeDrink(coffeeDrink) }
) {
Text(text = "—", style = TextStyle(fontSize = 14.sp))
}
}
Text(
modifier = LayoutFlexible(1f) + LayoutPadding(top = 6.dp, bottom = 8.dp),
text = coffeeDrink.count.toString(),
style = TextStyle(fontSize = 18.sp, textAlign = TextAlign.Center)
)
Clickable {
Button(
modifier = LayoutWidth(40.dp),
backgroundColor = Color.Transparent,
elevation = 0.dp,
onClick = { onAddCoffeeDrink(coffeeDrink) }
) {
Text(text = "+", style = TextStyle(fontSize = 14.sp))
}
}
}
}
}
}
private fun addCoffeeDrink(orderCoffeeDrink: OrderCoffeeDrink) {
if (orderCoffeeDrink.count < 99) {
orderCoffeeDrink.count++
}
}
private fun removeCoffeeDrink(orderCoffeeDrink: OrderCoffeeDrink) {
if (orderCoffeeDrink.count > 0) {
orderCoffeeDrink.count--
}
}
After it, we can implement a list item.
@Composable
fun OrderCoffeeDrinkItem(
orderCoffeeDrink: OrderCoffeeDrink,
onAddCoffeeDrink: (OrderCoffeeDrink) -> Unit,
onRemoveCoffeeDrink: (OrderCoffeeDrink) -> Unit
) {
Container(padding = EdgeInsets(left = 16.dp, top = 8.dp, right = 16.dp, bottom = 8.dp)) {
Row {
Logo(orderCoffeeDrink)
Container(
modifier = LayoutFlexible(1f) + LayoutPadding(start = 8.dp),
alignment = Alignment.CenterStart
) {
Column {
Text(
text = orderCoffeeDrink.name,
style = TextStyle(fontSize = 24.sp),
modifier = LayoutPadding(bottom = 8.dp)
)
Text(
text = orderCoffeeDrink.description,
style = TextStyle(fontSize = 14.sp)
)
}
}
Container(width = 100.dp) {
Column {
Text(
modifier = LayoutPadding(bottom = 4.dp) + LayoutWidth.Fill,
text = "€ ${orderCoffeeDrink.price}",
style = TextStyle(fontSize = 18.sp, textAlign = TextAlign.Right)
)
Counter(
orderCoffeeDrink,
onAddCoffeeDrink,
onRemoveCoffeeDrink
)
}
}
}
}
}
We pass a model object as a parameter to a composable function. So, let's create a model class which help us to store a list of coffee drinks. Here we should use the ModelList<T>
class if we want to have a trackable List<T>
and have the possibility to redraw changes or draw a new element in the list. However, if we use List<T>
and modify data in the list the composable function wouldn't be re-invoked and the UI will be the same without any modifications.
@Model
data class CoffeeDrinkOrder(
val coffeeDrinks: ModelList<CoffeeDrink>
) {
val totalPrice = coffeeDrinks.map { it.count * it.price }
.sum()
}
As you can see here we have a totalPrice property. When coffee drinks property will be changed, the composable function will be re-invoked.
So, let’s create a function for displaying a list of data with coffee.
@Composable
fun OrderCoffeeDrinkScreen() {
Column {
AppBar(title = "Order coffee drinks") {
navigateTo(Screen.CoffeeDrinks)
}
OrderSummary(coffeeDrinkOrder.totalPrice)
Container {
AdapterList(data = coffeeDrinks) { coffeeDrink ->
OrderCoffeeDrinkItem(
orderCoffeeDrink = coffeeDrink,
onAddCoffeeDrink = { addCoffeeDrink(it) },
onRemoveCoffeeDrink = { removeCoffeeDrink(it) }
)
}
}
}
}
All composable functions will be called. However, if objects in the list are the same, the UI wouldn't be re-drawn. If a property was changed the relevant part of UI will be updated.
Let's update a bit the previous example and add an OrderSummary
composable function for displaying the total price.
@Composable
fun OrderSummary(totalPrice: Double) {
Surface(color = Color.DarkGray) {
Row(modifier = LayoutPadding(16.dp)) {
Text(
text = "Total cost",
modifier = LayoutFlexible(1f),
style = TextStyle(fontSize = 24.sp, color = Color.White)
)
Text(
text = "€ $totalPrice",
style = TextStyle(fontSize = 24.sp, color = Color.White)
)
}
}
}
In this case when we increment quantity for espresso coffee, only quantity and summary elements will be updated.
The last part is adding implementation of the AppBar
.
@Composable
private fun AppBar(
title: String,
onBackClick: () -> Unit
) {
TopAppBar(
title = { Text(text = title, style = TextStyle(color = Color.White)) },
color = Color(0xFF855446),
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
icon = ImagePainter(imageResource(id = R.drawable.ic_arrow_back_white)),
tint = Color.White
)
}
}
)
}
Summary
The Jetpack Compose has a powerful mechanism of re-invoking composable functions which based on state know which part of the data has been changed and redraw only the updated part the UI. Currently, with the traditional approach of building UI in Android, a lot of work connected with redrawing part of the UI was carried out by a developer. Jetpack Compose does this work under the hood and simplifies UI development and we will have fewer places for potential bugs.
We can create a trackable object by framework with @Model
annotation. In a case when we want to store multiple objects in a list we can use ModelList type which allows us to have a trackable list and redraw new or updated parts of the UI when needed.
Full source code you can find on GitHub.