[MUSIC PLAYING]
[APPLAUSE]
ALEJANDRA STAMATO: Hi there, everyone.
I'm Alejandra Stamato, engineer on the Android Developer
Relations team.
Here, we'll try to find the answer to a question
where to hoist state in Compose.
And what state?
We're referring to UI state, or what
you need to display in your app together
with the internal state of UI elements.
In any Android app, there is also two types of logic,
UI logic, or how to display your state
on the screen-- for example, how to show
you a list of messages-- and the business logic,
which are the rules that make your app your app,
like, where do these messages come
from, and the other sources.
Where you hoist your state will be dependent on what type
of state you need to apply to this state,
what type of logic that reads or writes your state.
So let's begin with a case of your UI state
living in composable--
let's begin with a case of having
to apply UI logic to your UI state,
where that logic lives in your composable functions.
Perfect.
That's it.
OK, so let's consider this chat app.
We have a bubble that expands and collapses on top.
If you go to composable functions,
we have a showDetails variable.
It's an internal variable, and the logic applied to it
is very simple.
So hoist in this state in this case
wouldn't bring that much benefit.
So we leave it here, making the composable function
the owner of this state.
So as you can see, not hoisting is a valid option.
But not hoisting your state makes your UI less reusable,
and there are some cases in which
we need to apply UI logic to it before sending it forward
to other composable functions.
And that's why we need state hoisting.
So let's consider a LazyColumn, what a LazyColumn does.
LazyColumn defines LazyListState parameter
with a default value given by rememberLazyListState.
This means that any column composable
can define it outside and bussing it down
if they need to.
Now, let's say we want to implement two things.
The first in our chat app, we want
a button to jump to the bottom of the list.
And for this, the button needs access to the list state.
And also, we want to scroll to the bottom of the list
after typing a new message.
And for this, the user input needs access too.
Right now, the current owner of the LazyListState
is the LazyColumn.
So what we want to do is to hoist it
to at least the conversation screen, so that can
be forwarded then to the message list,
and the user input can call UI logic, and the same thing,
the message lists come forwarded to whoever requires it.
In our code, what this turns out to is,
you go to a conversation screen.
You will find the state with LazyList methods,
pass it down to a message list.
You call UI logic in the user input.
And then the message list defines it
with a default value of rememberLazyListState,
just like we saw that LazyList does, as a best practice
to increase the stability.
And then we forward it back to the LazyColumn,
and the JumpToBottom button calling UI logic to it.
What we've done so far is just hoisting the state as required,
and changing who the owner is will
avoid passing it to composables that don't need it.
We recommend placing your state in the lowest common ancestor
between all the composables that read or write this state.
But when your logic becomes complex,
you can abstract your state and logic
into a plain class, which will also
make your composables simpler and allow testing that UI
logic in isolation.
And we have an example of that all along.
LazyListState abstracts the state of a LazyColumn.
It's stored in the scroll position for this UI element.
And also, it exposes methods to apply UI logic to it,
like ScrollToItem, AnimateScrollToItem, et cetera.
So we covered different cases of where
to put your state when you have UI logic applied to your UI
states, composables, some plain state holder class.
Now we'll see what happens when we
have business logic involved.
Our ViewModel is a great choice as implementation
of the screen level state holder, which
handles the business logic.
So what we want to do now?
Now, we want to fetch user suggestions in a chat space
when the user types @ and a hint, and of course,
these suggestions come from the data layer,
so what we want to do is--
and obviously, this is considered business logic.
So what we want to do is have the input message, or the text
field state, hoisted to at least a ViewModel, which
now obviously takes it outside of the composition.
You're hoisting your state outside of the composition.
And then your state can be accessed from the conversation
screen, and you can also call UI logic through the ViewModel's
exposed methods too.
Notice that now the ViewModel is the lowest common ancestor
and owner of this state.
In code, we go to our ViewModel.
We define inputMessage.
We compose APIs.
And by doing so, inputMessage also
survives configuration changes because of the ViewModel's
scope.
And for our suggestions, we find a MutableStateFlow,
apply some UI logic as you receive a new inputMessage,
and that's it.
You have that, and meaning, the UI collecting from this flow
receives any suggestions, updates the UI,
and everyone's happy.
So what we've seen is that when we have business logic being
applied to UI state or producing UI state,
you should hoist that state to your screen level state holder,
a ViewModel in our case.
And that's our table completed.
As a recap, where your hoist your state
will be determined by the type of logic that needs your state
and that needs to write or read that state.
And we've explored several examples
from having the composables at the bottom
as a source of truth, to hoist any higher
to extracting or hoisting your state to a plain state holder
class, or to the ViewModel model outside of the composition.
We recommend keeping the state as low as possible, closer
to where it is consumed, and exposing immutable state
and events to modify the state from the owner
for the consumers.
Some more resources for you to learn more
include the UI state holders and UI production
talk from [INAUDIBLE] today, and the architecture documentation.
That's it.
Thank you.
[APPLAUSE]
[MUSIC PLAYING]