0

Fun with State in Jetpack Compose

 1 year ago
source link: https://blog.kotlin-academy.com/fun-with-state-in-jetpack-compose-745c1174cab6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Fun with State in Jetpack Compose

0*I4mA9VlRkQaGxuUJ

Photo by Adam Jang on Unsplash

TL;DR

When working with Jetpack Compose, make sure that data, that has to be reflected in the view immediately, is stateful and hence will trigger a recomposition when changed.

Jetpack Compose on Android is a marvelous tool. You can now use it to build any UI element you want, no matter how complex or simple it is. It is much easier to prototype in it too. Gone (almost) are the days of XML layouts where you would connect all the dots yourself and then make sure that elements get redrawn when you want them to, while also making sure that they are referenced properly, as well as disposed of, when not needed anymore. Having done that, and still doing it sometimes due to some legacy codebases, I really do appreciate the goodies Compose gives us, with my favorites being:

  1. Reduced lines of code, since we can now write beautiful UI with much less as common elements are given to you out of the box and you can easily expand on those and create new ones should you miss any.
  2. Declarative API, which allows us to understand the code with easy since it is written in a very logically structured manner.
  3. Fast development time, since we can use the provided Preview annotations and PreviewProviders that allow us to immediately see the changes we make to the layouts.
  4. Easy animations. This is a huge one since it used to be a pain to create animations in XML and with Compose it often needs only a line or two.

However, just like with any new tool, there are things you have to learn and remember, and Compose is not an exception to that rule. Yes, it is powerful, but if you abuse that power, even the increased speed of development will not save you.

Here are two files that we will talk about. Will refer to smaller snippets as we go.

This app is pretty simple. We have a list of people, who are coming to your yearly get-together. Those are all the people who have already told you that they will be arriving and they have told you, where they will be arriving from. Exciting!

We have a few things that we can do with the list:

  1. Add a new person
  2. Remove an existing one
  3. Amend the city they are coming from
  4. Mark the line for export (no actual export functionality)
1*EHGo_Md3ETLKQNaCxcUxZg.png

The way that Compose works is that the view reacts to the data that changes and once the change is detected, a recomposition is triggered.

Recomposition is the process of calling your composable functions again when inputs change. This happens when the function’s inputs change. When Compose recomposes based on new inputs, it only calls the functions or lambdas that might have changed, and skips the rest. By skipping all functions or lambdas that don’t have changed parameters, Compose can recompose efficiently.

So it only makes sense that when we remove a row by using the remove button, it will change the list and recompose, right? Let’s look at the code and behavior.

Button(
modifier = Modifier
.padding(3.dp)
.weight(1f),
onClick = {
onRemove.invoke(index)
}
) {
Text(text = "Remove")
}

This is the button code that calls a callback called onRemove, when a user clicks it.

fun removePerson(position: Int) {
peopleState.removeAt(position)
}

Nothing to it. Since we change the list size when removing the element and the list is a stateful variable, as shown, it will trigger the recomposition:

val peopleState = mutableStateListOf(
Person("James", "London"),
Person("Mantas", "Vilnius"),
Person("Rick", "Stockholm"),
Person("Jeanette", "Aarhus"),
Person("Martin", "Paris"),
Person("Diego", "Rio"),
Person("Vytautas", "Kaunas")
)

Let’s see it in action.

1*qzRepdaj2J0gzTXDPmpAmw.gif

The same happens when we want to change the list by adding an element to it. Check it out:

1*DtrqGdig7XP9EndofBYLuQ.gif

And some code:

fun addPerson(name: String, city: String) {
peopleState.add(Person(name = name, travellingFrom = city))
}

So essentially the same, as we are only changing the length of the list. Since the list is a mutableStateList, it will notify the system to recompose what is seen on the screen.

Now onto something more interesting!

Let’s change the city name, just to see if it will reflect on the screen.

1*ZMKM69U__JxbpIuZeHLxYg.gif

We changed the name of the city and it changed immediately after we committed the change via the Add button, right? Wrong. We were changing a piece of data inside of the list item, which does not fall under a “state”, hence why it will not trigger a recomposition. To prove that, let’s comment out the line that hides the dialog and put a breakpoint on when the change gets committed to the item in the list.

@Composable
fun PersonItem(
index: Int,
person: Person,
onRemove: (Int) -> Unit,
onChangeCity: (String) -> Unit,
onTagged: () -> Unit
) {
val showDialog = remember {
mutableStateOf(false)
}
// Omitted code
if (showDialog.value) {
ChangeCityDialog(person.travellingFrom) { city ->
onChangeCity.invoke(city)
// showDialog.value = false
}
}
}
1*9NJgA5k35xlQSy9gclo7dQ.gif

Now the dialog does not hide and we see that even though we change the city name, it does not reflect in the list visible on the screen. Has it changed though? It has and if we look at the debugger, we can see that it works properly.

1*gao8Hs4ljMmI13Bco62v5Q.png

So why did it work before? It worked not because the data change triggered the recomposition, but because the view change triggered it. When we hide the dialog, the screen has to recompose to see the latest data, and with that, we take in the latest data in the list. Because it happens so quickly, we wrongly assume that the data change was behind that.

Let’s see the last example.

In the following, when we click on any line, it should become highlighted.

1*5jF3rp-0qVigrk5CrQ4wCg.gif

It’s clicked, why nothing happens? We got angry and removed Vytautas from the list.

1*9JDolVGa8IdEmF0X9XGbrg.gif

What happened here? Well, we did highlight the row on the data side of things, but the recomposition did not happen. But once the last row was removed, it got triggered and took in the latest data, which includes the highlight indicator.

Let’s fix that!

The person data class needs a small change.

class Person(val name: String, var travellingFrom: String) {
var tagged = false
}

Must be changed to:

class Person(val name: String, var travellingFrom: String) {
var tagged by mutableStateOf(false)
}

That results in:

1*Pclvsq-t4eSYiPLBiFyvpg.gif

Since we now have a state variable, changes to it trigger a recomposition.

Hopefully, now you have a better understanding of how recomposition works and you will save time while debugging.

Sorry for the long post and I hope you learned something useful!

Till next time!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK