Jetpack Compose

Jetpack Compose: State

Alex Zhukovich 5 min read
Jetpack Compose: State
Table of Contents

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.

Coffee drink card

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.

Coffee drinks app

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.

Resources


Mobile development with Alex

A blog about Android development & testing, Best Practices, Tips and Tricks

Share
More from Mobile development with Alex
Jetpack Compose: Divider
Jetpack Compose

Jetpack Compose: Divider

This article covers using and customizing the “Dividers” components from the "Material 2" and "Material 3" libraries in the Jetpack Compose. In addition to that, we will explore the difference between implementation of the Divider, HorizontalDivider and VerticalDivider.
Alex Zhukovich 4 min read

Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to Mobile development with Alex.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.