Jetpack Compose: Getting Started

Aug 1 2023 · Kotlin 1.8.10, Android 13, Android Studio Flamingo

Part 3: Jetpack Controls

17. State Management in Jetpack Compose

Episode complete

Play next episode

Next
About this episode
Leave a rating/review
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 16. Display Lists Using Lazy Layouts Next episode: 18. Understanding Recomposition

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 17. State Management in Jetpack Compose

So far, we have been able to draw out different items on the UI. However, we have not been able to interact with these items.

An example of this is the TextField Composable. We can draw out a TextField on the UI, but we cannot type into it.

This is because we have not defined any state for the TextField. In this episode, we will learn how to manage state in Jetpack Compose.

We will get to define what state is and how it affects rendering on the UI. We will also learn how to use the remember API to manage state in our apps.

Before we get into managing state, let us first define what state is.

State is data that can change over time. This means that the value of state can change at any point in time.

For example, the value of a TextField can change at any point in time. This means that the value of a TextField is state.

Jetpack Compose helps us in becoming explicit about where and how you can store and use state in your app.

Since Jetpack Compose is declarative, the only way to change the UI is call the same Composable with different parameters.

Thes parameters are usually a representation of the state of the UI. This means that the UI is a function of the state.

This is why we need to be explicit about where and how we store state in our apps.

Remember our SearchBar Composable from one of our previous episodes? We are unable to type anything into the TextField because we have not defined any state for it.

There are two terms that we need to understand when it comes to state in Jetpack Compose. These are:

  • Composition - This is the process of drawing out the UI. This is done by calling the same Composable with different parameters.
  • Recomposition - This is the process of recomposing the UI. This is done when the state of the UI changes.

Now that we have defined what state is, let us learn how to manage state in Jetpack Compose.

Composable functions can use the remember API to store state. This API is used to store state that is scoped to a Composable.

A value computed by remember is usually stored during the first composition. This value is then returned during recomposition. These values can either be mutable or immutable.

The remember API is usually used to store state that is scoped to a Composable. This means that the state is only available to the Composable that it is defined in.

There are three ways to use the remember API to declare MutableState. These are:

  •   val state = remember { mutableStateOf(default) }
    
  •   val state by remember { mutableStateOf(default) }
    
  •   val (state, setState) = remember { mutableStateOf(0) }
    

These declarations are all equivalent. The first two declarations are used to declare a MutableState variable. The third declaration is used to declare a MutableState variable and a setState function.

The setState function is used to update the value of the MutableState variable. This function is usually called in response to an event.

Add State to our SearchBar

Let us add a MutableState variable to our SearchBar Composable. This variable will be used to store the value of the TextField.

@ExperimentalMaterial3Api
@Composable
fun SearchBar() {
    var search by remember { mutableStateOf("") }

    Card(
        modifier = Modifier
            .fillMaxWidth()
            .height(75.dp)
            .padding(end = 16.dp, start = 16.dp, top = 10.dp),
        shape = RoundedCornerShape(100.dp),
    ) {

        TextField(
            modifier = Modifier.fillMaxSize(),
            value = search,
            onValueChange = { search = it },
            leadingIcon = { Image(painter = painterResource(id = R.drawable.ic_search), contentDescription = "search bar") },
            trailingIcon = { Image(painter = painterResource(id = R.drawable.ic_filter), contentDescription = "filter") },
            placeholder = {
                Text(
                    text = "Search for food ...",
                    modifier = Modifier
                        .fillMaxHeight(),
                    textAlign = TextAlign.Center,
                )
            },
            colors = TextFieldDefaults.textFieldColors(
                focusedIndicatorColor = Color.Transparent,
                unfocusedIndicatorColor = Color.Transparent,
            ),
        )
    }
}

Add HomeSection

@Composable
fun HomeSection() {
    Surface(modifier = Modifier) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(top = 16.dp)
                .padding(horizontal = 8.dp),
        ) {
            // Categories header
            Row(
                modifier = Modifier
                    .fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween,
            ) {
                Text(text = "Food Category")
                Row(modifier = Modifier, verticalAlignment = Alignment.CenterVertically) {
                    Text(
                        text = "See more",
                        color = MaterialTheme.colorScheme.primary,
                        fontWeight = FontWeight.Bold,
                    )

                    Spacer(modifier = Modifier.width(10.dp))

                    Image(
                        painter = painterResource(id = R.drawable.ic_right),
                        contentDescription = "Right",
                        colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.primary),
                    )
                }
            }

            // Categories list
            LazyRow(
                modifier = Modifier
                    .padding(top = 8.dp)
                    .fillMaxWidth(),
            ) {
                items(getCategories()) {
                        item ->
                    FoodCategoryItem(icon = item.image, category = item.name)
                }
            }

            Spacer(modifier = Modifier
                .fillMaxWidth()
                .height(32.dp))

            Row(
                modifier = Modifier
                    .fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween) {

                Text(text = "Food For You")

                Row(modifier = Modifier
                    .width(40.dp)
                    .align(Alignment.CenterVertically),
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.SpaceBetween) {

                    Surface(modifier = Modifier
                        .size(10.dp),
                    shape = CircleShape,
                    color = MaterialTheme.colorScheme.primary) {

                    }

                    Surface(modifier = Modifier
                        .size(10.dp),
                        shape = CircleShape,
                        color = MaterialTheme.colorScheme.onBackground) {

                    }

                    Surface(modifier = Modifier
                        .size(10.dp),
                        shape = CircleShape,
                        color = MaterialTheme.colorScheme.onBackground) {

                    }
                }

            }

            Spacer(modifier = Modifier
                .fillMaxWidth()
                .height(8.dp))

           // For You - Food Items
            LazyRow(
                modifier = Modifier
                    .fillMaxWidth(),
                contentPadding = PaddingValues(5.dp)
            ){

                itemsIndexed(getFood()){ index, item ->
                    FoodItem(id = index ,food = item)
                }

            }


        }
    }
}

Add PriceBar

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PriceBar(
    price: Double? = 0.0,
    originalPrice: Double = 0.0
) {

    var qty by remember { mutableStateOf(1) }

    Box(modifier = Modifier
        .height(60.dp)
        .fillMaxWidth()){

        Row(modifier = Modifier) {

            Text(
                text = "$${ price?:originalPrice }",
                style = MaterialTheme.typography.displaySmall,
                color = MaterialTheme.colorScheme.primary)

            if (price != null){

                Text(
                    modifier = Modifier.padding(start = 5.dp),
                    text = "$${originalPrice}",
                    fontWeight = FontWeight.Medium,
                    textDecoration = TextDecoration.LineThrough
                )

            }
        }


        Box(
            modifier = Modifier
                .width(150.dp)
                .fillMaxHeight()
                .background(
                    color = MaterialTheme.colorScheme.tertiary,
                    shape = RoundedCornerShape(35.dp)
                )
                .clip(RoundedCornerShape(35.dp))
                .align(Alignment.CenterEnd),
        ) {

            Row(
                modifier = Modifier
                    .fillMaxSize(),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                
                FloatingActionButton(onClick = { qty = if (qty > 0) --qty else 0  },
                    modifier = Modifier
                        .padding(start = 5.dp)
                        .size(45.dp),
                    shape = CircleShape,
                    containerColor = MaterialTheme.colorScheme.primary,
                    contentColor = MaterialTheme.colorScheme.onPrimary) {

                    Image(
                        painter = painterResource(id = R.drawable.ic_remove),
                        contentDescription = "Add")
                }

                TextField(
                    value = "$qty",
                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number,),
                    onValueChange = { },
                    modifier = Modifier
                        .width(45.dp),
                    colors = TextFieldDefaults.textFieldColors(
                        textColor = MaterialTheme.colorScheme.onTertiary,
                        containerColor = MaterialTheme.colorScheme.tertiary)
                )
                
                FloatingActionButton(onClick = { qty++ },
                    modifier = Modifier
                        .padding(end = 5.dp)
                        .size(45.dp),
                    shape = CircleShape,
                    containerColor = MaterialTheme.colorScheme.primary,
                    contentColor = MaterialTheme.colorScheme.onPrimary) {

                    Image(
                        painter = painterResource(id = R.drawable.ic_add),
                        contentDescription = "Add")

                }

            }

        }

    }

}

State across Configuration Changes

remember helps you to retain state during recompositions, but it doesn’t help you to retain state across configuration changes. For example, if you rotate the device, the state will be lost. To retain state across configuration changes, you can use rememberSaveable.

@Composable
fun Counter() {
    var count by rememberSaveable { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("I've been clicked $count times")
    }
}

Jetpack Compose does not force you to only use MutableState<T> for state. You can also use other observable types, however, you will be required to convert it into a State<T> object. The supported types are:

  • Flow - This can be used using either collectAsState or collectAsStateWithLifecycle methods.
  • LiveData - This can be used using the LiveData.observeAsState method.
  • RxJava2 - This can be used using the subscribeAsState method.
  • RxJava3 - This can be used using the subscribeAsState method.

In order to use these other state types, you will need to add their relevant dependencies to your project.

In this section, you learned about state in Jetpack Compose. You learned about the different types of state, how to create state, and how to use state in your composable functions. You also learned about how to retain state across configuration changes.

In our next session, we will look at how to handle recompositions in Jetpack Compose.