(gentle music)
- [Florina] In this screencast,
you're going to learn about how state works in Compose,
what are some of the APIs you can use,
and how state and events should flow in your app.
Let's take a simple example.
The user can enter their name,
and a greeting should be displayed.
So we have a text for the greeting
and a text field for the name input.
If we run this now, we see that nothing happens.
That is because the text field doesn't update itself.
It updates when its value parameter changes.
To do that, we need to pass in a value
that represents the state of the text field,
and then update the state
when the text field notifies us of the change.
So let's introduce a local state
that holds the name that should be displayed.
To do this, we will use remember of mutableStateOf,
passing in the default value for the text.
MutableStateOf creates a mutable state,
which is an observable type in Compose.
Any changes to its value will schedule a re-composition
of any composable functions that read that value.
So in our case, whenever the name state changes,
the value displayed by TextField will change, too.
Remember helps us preserve the state across re-compositions.
Otherwise, if we would just use mutableStateOf
without remember,
every time our HelloContent composable gets recomposed,
the state gets reinitialized to an empty string.
We can use the remembered value
as a parameter for other composables,
or even as logic in statements
to change what composable's already displayed.
For example, we don't want to display the greeting
if the name is empty, so we can use the state
in an if statement.
While remember helps us retain state across re-compositions,
the state won't be retained across configuration changes.
For this, we'll have to use rememberSaveable.
RememberSavable will automatically save any value
that can be saved in a bundle.
But if that's not the case,
you can pass in a custom saver object.
Right now, our composable holds its own state.
This makes the composable hard to reuse, hard to test,
and keeps the composable tightly coupled
to how the state is stored.
So we should name this a stateless composable:
a composable that doesn't hold any state.
To do this, we can use state hoisting.
State hoisting is a programming pattern
where you move the state to the caller of a composable.
A simple way to do it is by replacing the state
with a parameter and use functions to represent events.
The parameter is the current value to be displayed,
and the event is a lambda function that gets triggered
whenever the state needs to be updated.
In our case, we extract the name and the onValueChange
out of HelloContent and move them higher
to a HelloScreen composable that calls HelloContent.
HelloContent has access to the state
as an immutable string parameter
as well as a lambda onNameChange that it can call
when it wants to request the state change.
Lambdas are the most common way
to describe events on a composable.
Here, we're defining an event called onNameChange
using a lambda that takes a string
using Kotlin's function type syntax.
We're using onNameChange, present tense,
as the event doesn't mean
that the state has already changed,
but that the composable is requesting
that the event handler change it.
Like this, the state goes down
from HelloScreen to HelloContent,
and events go up from HelloContent to HelloScreen,
making HelloContent more reusable and testable than before.
But what if we want to use the name
in other layers of our app?
Maybe we want to save it in a database.
The best way to do this is to use a view model
that will store the state in an observable holder
and handle events.
So we can create a HelloViewModel
that extends the ViewModel class.
The view model holds and exposes the name in a LiveData,
which then can be observed as state
and passed to a composable.
Like this, every time the value in LiveData changes,
it will automatically trigger a re-composition
of HelloContent.
As the view model survives configuration changes,
we don't need to do anything else to persist the UI state.
Under the hood,
observeAsState will also remember the state for it
so it can survive re-composition.
Then, we also hoist the event handling
to the view model.
Like this, the view model is the one that updates the state
every time an event happens.
As the UI state can be changed from other layers of the app,
like based on the response of a network call,
the view model allows us to better encapsulate the state
and create a single source of truth for the UI,
which makes it less likely to create inconsistent states.
Okay, let's recap.
If your state is internal to a composable,
make sure you use "remember"
to persist the state across re-composition.
Use "rememberSaveable" to persist the state
across configuration changes.
Where possible, try to hoist the state
to make the composable more reusable and testable.
Finally, use the "ViewModel" to hold an exposed state
in an observable state holder like LiveData
and to handle events.
To learn more about state in Compose,
check out the documentation
and the CodeLab link in the description below.
Thanks for watching.
(gentle music)