[MUSIC PLAYING] ALEJANDRA STAMATO: Hi there. I'm Alejandra, engineer on the
Android Developer Relations team. MANUEL VICENTE
VIVO: Hi, everyone. I'm Manuel-- same
team, same title. ALEJANDRA STAMATO: Welcome to
advanced state and Side Effects in Jetpack Compose workshop. In this code lab, you will
learn all about complex state management in Compose
and side-effect APIs. MANUEL VICENTE VIVO:
Yeah, in particular, you will learn how
to produce state and observe streams of data
in Compose to update the UI, how to create a state holder
for stateful composables, side-effect API's,
such as LaunchedDefect, RememberUpdatedState,
reviewState, and deriveStateOf, and also how to
create coroutines and code Suspend
functions in composables using the
rememberCoroutineScope API. Quite exciting, isn't it? ALEJANDRA STAMATO: So
you might remember us from another code
[INAUDIBLE],, in which we built a wellness app. If you haven't solved
the [INAUDIBLE] codelab and are unfamiliar with basic
handling state in Compose, with APIs like
MutableState and Remember, make sure you check
that one first. You can find the link in
the video description below. MANUEL VICENTE
VIVO: Today, we will work on an unfinished
application [INAUDIBLE] material study app which
consists of our travel search app that helps you find flights,
hotels, and restaurants given different parameters, like
amount of people, location, and dates. We'll go step by step
through the codelab, adding features and
refactoring the code to improve it-- for
example, populating a list of suggested destinations
and adding a splash screen to the app. ALEJANDRA STAMATO: So I'm
going to be sharing my screen with a combo of text, Android
Studio, and the emulator so you can easily follow
along as we solve it. As always with
codelabs, we'll advise that you try to solve it as
you watch or maybe later. But do try to solve it yourself
so all the concepts really stick. Also, I already
completed step number two of the codelab, which is
setting up the environment and downloading the app Start
Code in the main branch. MANUEL VICENTE VIVO: OK, now
that we know what we need, what we are building, and
we have the code ready, let's take a look at the UI
state production pipeline section. ALEJANDRA STAMATO:
So the first feature that we want to implement in
creating our travel sample app is populating the list
of suggested destinations so it looks like our design. If we run the app from
a main branch right now, we see that this list is empty. And to fix this, we have to
complete essentially two steps. Add the logic to the
View model to produce the UI state, which is the
list of suggested destinations. And consume the state from
the UI to show the list. Let's see how to do it. MANUEL VICENTE VIVO:
For any kind of app, we recommend implementing
a layered architecture to a very common, good
architectural principle like separation of concerns,
single source of truth, and testability. That's the architecture
you can see in this app. To populate the UI
with data coming from other layers
of the hierarchy, we use a UI state
production pipeline. This is a process in
which we typically access the data layer from the
View model or any other state holder, apply business
rules if needed, and then expose the
screen noise [INAUDIBLE] in an observable
data folder class. The UI consumes that UI state
and displays the information on the screen. If you're interested in learning
more about UI state production, check the link in the
video description. ALEJANDRA STAMATO: So the
data layer in this application is already implemented. What we have to do now
is use that data layer to produceState. So we can open the
main View model. And we can define a suggested
destination variable of type, MutableStateFlow, to
represent the list of suggested destinations and set an empty
list as the initial value. And this is a variable. It's private and will only be
mutated in this View model. And next up, we can define an
immutable variable, suggested destination just below,
of type state flow. This is the public
read only variable that can be consumed from the UI. And also, I'm using an
extension as state flow that will transform the mutable
state flow with defined V4 from mutable to immutable. MANUEL VICENTE VIVO: Yeah, this
is a general good practice. You ensure the screen UI state
cannot be modified unless it goes through the View model. Thus, we can say
that the View model is the single source of truth
for the screen UI state. ALEJANDRA STAMATO:
So in the ViewModels, any block now, what we can do
is just access a data layer, calling the
Repository.destinations and assign it to our prior
variable suggestedDestinations we defined before. And finally, we can
uncomment all the usages of suggestedDestination in this
class, for example here, here, and here. And these will make our
variable to be properly updated with events that are
coming from the UI. The screen has selectors, for
example, the Adult selector and Destination selector. And our suggested
destination will be updated when you change the amount
of people in this function or when you change, for
example, the destination in this other method. MANUEL VICENTE
VIVO: This is great. The first step is done. Now, the View model is
able to produce a UI state. Next up, we need to consume
the UI state from the UI. ALEJANDRA STAMATO: So
our list is still empty. So let's open CraneHome. And look at CraneHome contents. So CraneHomeContent composable
contains the BackdropScaffold. And this component allows
you to define a back layer content or the
part of the screen where the selectors are,
like the select number of adults and destination. And then you have
frontLayerContent, which is where our list of
suggested destinations will be. So we want the UI
to update and show the list of suggested
destinations here whenever there
is a new item emitted into the stream of data. And to help us do
that, we can use a collectAsStateWithLifecycle
API. MANUEL VICENTE
VIVO: To start using the collectAsStateWithLifecycle
API, first, add the Lifecycle Runtime compose dependency
to your app's d.gradle file. The variable lifecycle_version
is defined already in the project with the
right version, at least 2.6. Now we can sync the project. ALEJANDRA STAMATO:
Great, so now, we can go back to
CraneHomeContent. We can assign
suggestedDestinations to a call to
viewModel.suggestedDestinations, which is our flow. And then we use
collectAsStateWithLifecycle to collect from that flow. And then we can run the
app in the meantime. This API will collect the
values from the state flow and map the latest
values to Compose state in a lifecycle-aware manner. And this allows your app to save
app resources when not needed, such as when the app
is in the background. Now, the Compose codes
within this state value recompose on new omissions So we run the app now. We see that the
list of destinations is actually populated. Also, this list updates
thanks to the logic present in the View model. For example, when you change the
amount of adults, it changes. And when you change the to
destination-- for example, Argentina. MANUEL VICENTE
VIVO: Compose also offers APIs to transform
other stream-based solutions, like [INAUDIBLE] data
into Compose state. Check the Compose and Other
Libraries documentation to learn more about that. [MUSIC PLAYING] OK, so we just learned
how to produce the UI state from the state
folder using state flow and how to consume it in the UI. Let's move on to the
next step in the codelab. ALEJANDRA STAMATO: So next step
in this step of our project, we want to add a landing
screen to the app, which could be used to load all the
data needed in the background. In a real app starting
in Android 12, you should use
the Android Splash API instead of implementing
a custom splash. So keep it in mind that
this is just an example to illustrate the next API. So for a splash screen, we
want to show the full screen with the logo. Then there will be a delay
as a simulation of data being loaded in the background. And then we navigate to
the main Home screen. There is already a file
called LandingScreen, which we are going
to open now, which is where we can put the codes
that we need to implement. And somewhere in
our landing screen, we're going to make a call
to our backend to load data. And we can represent this if
we remove this with a call to delay on SplashWaitTime. The problem is that we cannot
just run a Suspend function here just like that. We have two problems. The first one is obvious. This does not
compile, as it says. Suspend Function Delay
should be called only from a [INAUDIBLE] or
another Suspend function. So we need another
API to help us here. But also, we shouldn't
just call this method like this in this composable
function because remember, composable functions
can run multiple times. This can make our backend
call run multiple times too, which can be expensive. And also, composables
can be restarted in the middle of the
call without you knowing. So for more on
how Compose works, you can check the Thinking
in Compose docs linked below. MANUEL VICENTE VIVO:
Calling the network to fetch data asynchronously
is a side-effect. And we need a way
to run side-effects in a safe and controlled
way in Compose. In general, a side-effect is a
change to the state of the app. When talking about
side-effects in Compose, we are referring to invoking
side-effects from a composable and altering the state of
the app outside the scope of that composable
function-- for example, opening a new screen when
the user taps on a button, showing a message
when the app doesn't have internet connection,
sending analytics events, or calling the network, like
is the case for this section of the codelab. ALEJANDRA STAMATO: OK,
so back to our code, how do we safely run the Suspend
function inside my composable? MANUEL VICENTE VIVO: To
call Suspend functions safely from inside the
composable, use the Launch Defect API, which triggers
a CoroutineScope side-effect in Compose. ALEJANDRA STAMATO: OK, so
I can wrap my Delay call in a LaunchedEffect,
like so, which launches a coroutine
with a block of code passed as a parameter. And the coroutine
will be canceled if LaunchedEffect leaves the
composition for any reason. MANUEL VICENTE VIVO:
LaunchedEffect takes keys as parameters that are used
to restart the defect whenever one of those keys changes. What happens is that the
ongoing coroutine is canceled. And a new coroutine is triggered
with the new state values. ALEJANDRA STAMATO: OK,
then I need the key. What if I use
onTimeout as my key? Will that work? And then I can
just call onTimeout after the delay to
trigger whatever happens after the data is loaded. MANUEL VICENTE VIVO:
Sure, that would work. In this case, if onTimeout
gets a new value, for example, when the parent
composable changes how the lambdas will behave,
then the effect would restart. However, for this
particular use case, it doesn't make that
much sense, right? We wouldn't want to
restart the effect when the lambda changes because
that will restart the delay. And it will take longer. Ideally, we would
like the effect to keep running even
if the lambda changes. ALEJANDRA STAMATO: OK, I can
fix this by changing the key. To trigger the side-effect
only once during the lifecycle of this composable, we can
use a constant as a key-- for example, LaunchedEffect
of [INAUDIBLE] MANUEL VICENTE VIVO:
Yeah, with the constant, the effect will be triggered
just once when this composable enters the composition. However, I think we still
have some issues to fix. If onTimeout changes while the
side-effect is in progress, there is no guarantee that
the new onTimeout is called when the effect finishes. We want the LaunchedEffect
to invoke the delay only once during the lifecycle of
this composable function and also ensure that we invoke
the latest onTimeout lambda value. ALEJANDRA STAMATO:
We can guarantee this by remembering onTimeout using
that rememberUpdatedState API. So it will capture and
update the newest value. So here, we define
currentOnTimeout with rememberUpdatedState,
which wraps onTimeout lambda. And now, we call
currentOnTimeout instead of onTimeout directly. MANUEL VICENTE VIVO: Exactly. rememberUpdatedState is
actually a very important API. It should be used when
parameters or values computed during composition
are referenced by a long-lived lambda or
object expression, which might be common when
working with side-effects. So as a general rule of
thumb, when using lambdas in a side-effect, always
think if you should be using a rememberUpdatedState. Most of the time, the
answer might be yes. If you want to restart effect
when the lambda changes, then don't use this API
and not the lambda as a key to the effect. ALEJANDRA STAMATO: Great. So when the landing
screen is completed, it loads data in the background. And after it's done, it calls
the currentOnTimeout, which calls the onTimeout lambda. So now, our second
step is to actually call the landing screen
somewhere to display it when the app is open. So we go to our main activity. We find the main screen,
which is the first screen that we showed in the
app, the first composable. And it will call
CraneHome, which contains search
parameters and the list of suggested destinations. So instead of just calling
CraneHome directly, we can define a variable,
showLoadingScreen, which is a variable that will
determine whether to show or hide the landing screen. And then we can write
an if statement. So if showLoadingScreen,
is true, we show the landing screen-- else, we just go to CraneHome. Now, landing screen receives
an onTimeout lambda parameter, which is called after the delay. So we want to change
showLandingScreen to be false, which will make
the composable recomposed. And when the
function recomposes, showLandingScreen is false. Then we navigate to CraneHome. We ran it very fast. But we can run it again to
see that if we run the app, we show the landing
screen for two seconds. And then we navigate
straight into our CraneHome. MANUEL VICENTE VIVO: Cool, nice. That's a pretty
nice splash screen. We've implemented it like
this for learning purposes. But again, if you want
to add a proper splash screen to your app, consider
the SplashScreen API that was added in Android 12. It also provides
a compact library that supports older versions--
link in the description. [MUSIC PLAYING] OK, we learned a lot of
things in this section of the codelab-- what a side-effect is and
how to use launch effect and rememberUpdatedState. What's next? ALEJANDRA STAMATO:
In this next step, we'll make the
navigation drawer work. Currently, nothing happens when
you try to tap the Drawer icon. So you will open
the CraneHome file. And we go to
CraneHomeContent here. There is an openDrawer event
that we need to complete. And we have a
scaffoldState defined with a rememberScaffoldState
remember function. ScaffoldState has a
draw state with methods to open or close the navigation
drawer programmatically. So we can just call
scaffoldState.drawer.open to open the navigation drawer. And that should be that. However, we see an error. And that's because open
is a Suspend function. And we get an error, the
same as we did before. We need to run this
in a coroutine. MANUEL VICENTE VIVO:
Actually, some Compose APIs are Suspend functions. Suspend functions help
represent concepts as they happen over time. And this is the case of
opening the navigation drawer. Open suspends the
execution of decrypting, where it's been called,
until it finishes. Then the coroutine
resumes execution. A Suspend function needs to
be called for my coroutine. But the openDrawer function is
just a simple regular function. And here, we cannot use
LaunchedEffect because we are not in a composable context. ALEJANDRA STAMATO: Right, so I
cannot use LaunchedEffect like before to run my Suspend
function because we get a different error, which is
LaunchedEffect can only be run in composition. MANUEL VICENTE VIVO:
Yeah, so how can we create a new coroutine here? ALEJANDRA STAMATO: So
ideally, with [INAUDIBLE] coroutine scope that follows
the lifecycle of the call site. And we can use that
rememberCoroutineScope API to create a coroutine scope. With this scope, you
can start coroutines when you're not in composition,
like for example, here. We just call Launch and run our
scaffoldState.drawerState.open. We just run the app. And then in the
[INAUDIBLE] drawerContent, we just call this function. And that's it. The scope will be
automatically canceled when this composable
leaves the composition. And if we run the app now, we
see that the navigation drawer works. And our drawer works. MANUEL VICENTE VIVO:
Cool, nice, thank you. The last part of this
section of the codelab explains the difference
between LaunchedEffect and rememberCoroutineScope. Make sure to check it out. [MUSIC PLAYING] Let's move on to the next
section of the codelab. We'll see how to create
a new state holder. In our app, if you tap
on Choose Destination, you can edit the field
and filter cities based on your search input. The text weight also changes
from regular to bold. There is a fair bit of
logic unstated there. Let's take a look and see
how we can improve this code. ALEJANDRA STAMATO: Great. So the composable for
Choose Destination is editable user input. The CraneEditableUserInput
composable takes some parameters,
like hints and caption. And caption corresponds to the
optional text next to the icon where you type your destination. In our case, it's this
To: string beside Spain. MANUEL VICENTE VIVO:
This implementation has some downsides. We can make it better. For example, the value of the
text field is not hoisted. And therefore, cannot be
controlled from outside. But also, the logic
of this composable could become more complex. And the internal state
could be out of sync. Something we can do
to alleviate this is create a state
holder responsible for the internal state
of this composable so that you can centralize all
state changes in one place. ALEJANDRA STAMATO:
Definitely, let's do that. Let's create a plain
state holder class name, EditableUserInput
in the same file. And let's see a little
bit what we have now. So this class has a text,
which is a mutableStateOf type string. And we define it
as mutable state because Compose tracks changes
to the values and recomposes when changes happen. And we expose also
an updateText method to modify the text instead of
making the text [INAUDIBLE] public. So now, this class is a
single source of truth to modify the text. The class also takes
an initial text, which is just dependency that
is used to initialize our text. And the logic to know if
the text is a hint or not is in the isHint
method here, which performs the check on demand. Now, if these logics get
more complex in the future, we only need to make
changes to this one class, EditableUserInputState. MANUEL VICENTE VIVO:
This will simplify things for our future self. I think it's a good
practice to follow. Compose offers this
type of classes for their own
composables as well. You might have
already encountered the scaffoldState and
LazyListState state holders. And actually, a typical
Compose pattern we can do now is providing a function
to remember the state, like rememberScaffoldState
or remember LazyListState. In this way, you can
create state more easily and keep the same instance
in the composition instead of creating
a new one every time. For our case, we could
call this function rememberEditableUserInputState. ALEJANDRA STAMATO:
Sure, let's implement that in this very same file-- remember,
EditableUserInputState. So we define
RememberEditableUserInputState function that will
remember an instance of EditableUserInputState It
also takes hints as a key. So the hint changes. And there is a recomposition. This class instance
will be recreated with the newest value. And now, if we only
remember this state, it won't survive
activity recreations. So we can use
rememberSaveable instead, which behaves
similar to remember. But now, the stored
value will also survive activity and
process recreation. MANUEL VICENTE VIVO:
rememberSaveable uses the safe instance
state mechanism under the hood, which stores
information inside a bundle. The bundle can store
primitive types without any further
instructions. But our state here is a
little bit more complicated. ALEJANDRA STAMATO: Exactly. We need to tell
rememberSaveable how to save and restore an instance
of this class using a saver. MANUEL VICENTE
VIVO: In our case, for the saver of the
EditableUserInputState class, we can use some existing
Compose API, such as ListSaver or MapSaver to reduce the amount
of code that we need to write. This function stores the values
to save in a list or a map, respectively. ALEJANDRA STAMATO: So
it's a good practice to place the saver definitions
close to the class they work with because they need
to be statically accessed. So let's implement it. We first define a
companion object in EditableUserInput
plain class. Then we can write a Saver. A Saver describes how an object
can be converted into something that is saveable. Implementations of a Saver need
to override two functions, Save and Restore. In this case, we use a listSaver
as the implementation detail of the Saver to
store and restore an instance of
EditableUserInputState. In the Save function, we
are storing all of the class parameters as a list. And in the Restore, you can
retrieve them by the position that they occupy on this list. We have our Saver completed. Now, we can go to
rememberEditableUserInputState, where we can use our new Saver. So we go and we write our
Saver as EditableUserInputState of Saver. And now, rememberSaveable
knows how to store and restore an instance of our class. But keep in mind,
because rememberSaveable will store state in a bundle,
which has limited size, you just store the minimum state
required-- very simple objects, IDs, or keys. And avoid storing heavy
objects or a list of objects. To know more, see the Saving
UI State documentations in the link below. MANUEL VICENTE VIVO: Nice. Let's see how this looks
like from the holder side. Instead of having the
state and the logic in the composable
function itself, we need to delegate that
complexity to the state holder. And to make this function
more reusable across the app and in test previews, we
should allow hoisting. ALEJANDRA STAMATO:
Yep, let's do that. We'll use EditableUserInputState
instead of Text and isHint. First, we want to replace
Hint with our state. And we can give
it a default value so that we don't always
have to pass state in simpler cases, which
is a best practice. And for this, we can use our
rememberEditableUserInputState that we defined before
with an empty Hint. We can also remove the
onInputChanged lambda event because all of the
state is hoisted now. So [INAUDIBLE] need to
know the input changed. They can define the state. They can hoist define the state
and pass it into this function. And next, we can remove
the internal state. We're not going to need
it because now, we have our state implemented like so. And now, it's a matter of just-- obviously, our function
doesn't compile. Just make the tweaks
necessary to use our state instead of
the internal state that we had before. So now, we write
state.isHint like so everywhere that we need. We are not going to need
this isHint call like here. In the value of the basic
[INAUDIBLE] we call state.text. And every time there
is a value change, we call state.updateText
with the latest value to update the text. And since we changed the API, we
need to check in on the places where it's called to
make sure we're passing the appropriate parameters. And if we go and find
the instances of this, we're going to see that
the only place that calls this composable is
ToDestinationUserInput, which we need to fix. But first, let's
quickly see where we are in our composable structure. Search content has three types-- Fly, Sleep, and Eat. Right now, we're in the Fly
tab of FlySearchContent. And on the selectors,
to change things like people or
destinations are here. And ToDestinationUserInput
is just the wrapper around CraneEditableUserInput. So now, we are in
ToDestinationUserInput composable. You should see a build error. We need to fix the parameters
because we no longer have Hint or onInputChanged. So let's start by removing
on InputChanged, like so. And for Hints, in
order to define a state with an initial value
different than the default, we can define the
state here with RememberEditableUserInputState
and pass the same Hint it originally had. We replace Hint
parameter with State. We're calling it State now and
passing EditableUserInputState. And that's it. That's our composable
compiling now. MANUEL VICENTE VIVO: Cool, nice. And thanks for
showing the diagram. It's much easier to see that. In ToDestinationUserInput, there
is some functionality missing. We need to notify colors
when the input changes so that they can react to it and
apply logic when that happens. Due to how the
app is structured, we don't want to hoist
the EditableUserInputState any higher up in the
hierarchy because we don't want to couple
other composables, like FlySearchContent, with it. How can we call the
onToDestinationChanged lambda from here and still keep
this composable reusable? ALEJANDRA STAMATO: So we
can trigger a side-effect every time the input
changes and call the onToDestinationChanged
lambda that we're not using yet. So let's define a current
onDestinationChanged variable and use
rememberUpdatedState to wrap the onToDestinationChanged
lambda, in turn. And this is exactly
what we did before because we are going to use
this inside a launchedEvent. Then, we will create
a launchedEffect. And using
EditableUserInputState as key-- and don't worry too
much about this now. We're going to explain it later. And the next code right
now, what we are doing is right here, observing-- whenever we have new
changes in our text, filtering out whenever
it's a Hint because we're not interested in that and
collecting every new event where there is a
new text and calling CurrentonDestinationChanged
with the new text. MANUEL VICENTE
VIVO: Oh, and there is actually a new API that
you are using there, right? snapshotFlow converts Compose
state objects into a flow. When the state read inside
snapshotFlow mutates, the Flow will emit the new
value to the collector. In our case, we want
to convert this data into our Flow to use the
power of Flow operators. With that, we filter when
the text is not a Hint and collect the emitted
items to notify the parent that the current
destination changed. ALEJANDRA STAMATO: Exactly. And now that the
code is complete, let me explain the
decisions that we made. So first, because we're
using a lambda instead of the LaunchedEffect, we use
the RememberUpdatedState API to guarantee that the
latest value passed to onDestinationChanged is used. And then we're passing
EditableUserInputState as the key to the
LaunchedEffect because we want the effect to restart-- if the toDestinationUserInput
composable is recomposed with a
new state instance. If we don't do this, even
if the function recomposes, this LaunchedEffect
might keep running using an incorrect version
of the latest state. And apart from seeing an
incorrect behavior in your app, you might also be leaking
all state in memory. MANUEL VICENTE VIVO:
From what you explained and what we've seen
so far, I think there are two rules of thumb
we can pretty much follow all the time. We already explained
the first one. And it's if you're using a
lambda inside a side-effect, consider wrapping it inside
a RememberUpdatedState. The second one is about
keys to use with effects. Almost every instance
used inside a side-effect should be considered
a key unless you know that the instance is
very unlikely to change. In our LaunchedEffect, we
are using the state instance to perform operations. That's why we are
adding [INAUDIBLE] ALEJANDRA STAMATO:
Exactly, and there are no visual changes in
this step of the codelab. But we've added a state
holder to a UI element to make it more robust,
encapsulating state and UI logic to improve
its reusability. If we run the app now, we
should see that everything is working like it did before. [MUSIC PLAYING] In this section, we'll improve
how the Detail screen starts. Whenever we go and
click on a city, we show the Details screen. This is what we'll be working. So we go to Details Activity and
find the Details screen, which represents the whole screen. And we see that we
get the cityDetails by calling
viewModel.cityDetails, which has the city name,
country, description, and image URL. And then if the
result is successful, we just show that composable's
DetailsContent-- else, we show an error. Now, imagine if
cityDetails takes longer to load because it
requires a lot of complex data. You don't want your user staring
at a blank screen for too long. In this case, what we could do
is add a loading screen to show what data is loading and
when the data is ready, display the Details content. Let's see how we can build that. MANUEL VICENTE VIVO: One way to
model the state of the screen is with a data
class that exposes multiple but related
pieces of state, data to display on
the screen and then loading network signals. ALEJANDRA STAMATO: So this
class, Details UI State, can have the
information we need. We have city details, like name,
the coordinates of the city. It's loading to represent
if the screen is loading. Or if we get an error, we
have Boolean throw error. Then in the DetailsViewModel,
we could define a new pair of variables, like so. And uiState is a StateFlow. Just like we have before,
we expose a public version of a variable with a private
backing variable as well. And this Flow can be updated
inside the DetailsViewModel when the information
is ready or fails. And then in the UI,
Compose can collect from the stream with
collectAsStateWithLifecycle API that you already know about. But let's do something
different this time. MANUEL VICENTE VIVO:
Yeah, for the sake of this exercise of
learning a new API, we are going to implement an
alternative to the UI state production solution we
saw in the first steps. Instead of producing the screen
UI state in the View model, now, we are going to do
it in Compose code using the produceState API. produceState allows you to
convert non-Compose state that you can get from other
layers of the hierarchy into Compose state, so that
composables can recompose when that state changes. I think it's interesting
to see how this API works because these other
stream wrappers, like
collectAsStateWithLifecycle, use it under the hood. ALEJANDRA STAMATO: So let's
rewrite this code entirely. So first, let me clear
this part of the code. And let's define our UI
state with the produceState to emit UI state updates. So this API receives
an initial value. And for this, we can
send DetailsUiState with an isLoading value of True. So the first thing the user
will see when opening the screen is the Loading screen. And then we have a
lambda producer call. So the first thing we can do
is, just like we had before, have a call to
viewModel.cityDetails. And then we can assign
our value variable. So if cityDetail's
result is successful, we omit DetailsUiState with the
result of loading that data, else, we omit DetailsUiState
with a True value for throwError because
there's been an error. MANUEL VICENTE VIVO: Cool. produceState
launches a coroutine scoped to the composition
that can push values into their returned state using
the value property that you can see there. Because we are
inaccurate in context, we can do asynchronous calls
from here and update our state. ALEJANDRA STAMATO:
So finally, let's add a when statement where
we map different values of the DetailsUiState
to composables. Depending on the UI state,
if UiState.cityDetails is not known, it will show the details. If the value is loading, it
will show a circular progress indicator, which is
our loading spinner. Else, we show the
Error composable. Now, if we run the app now, you
won't see probably the loading screen because for now,
loading is too quick for us to see any progress bar. But if the code was doing
something more interesting, like fetching data
from the network, you would see it spin, as it
would take a little bit more to load. So just to test it,
what we can do is just add a fake delay to
our producer call. And we can run the
app in the meantime to imitate what would happen
if this call took longer. And we can add the delay
directly to the producer call as Manuel was saying
before, because produceState launches the coroutines
[INAUDIBLE] to the composition. And now, if we go and open
any city, we see the spinner. And then we navigate
to the Details screen. MANUEL VICENTE VIVO: Cool, nice. Our recommendation is
to produce the screen UI state in a view model class
that can access the data layer. But if your UI
state is very simple and doesn't need to communicate
with other layers of the app, produceState is the best
Compose alternative. You can learn more about this
in the UI State Production Architecture documentation. [MUSIC PLAYING] ALEJANDRA STAMATO:
Well, I think we're ready to move on to
our last and final API. The last improvement we
are going to make to Crane is showing a button to
scroll to the top in the list of suggested destinations. It will show this button
when you scroll the list and you pass the first
element on the screen. Tapping the button takes you to
the first element on the list. So let's open Explore section. And let's see the
ExploreSection composable, which contains what we see
in the bottom sheet, like the section title,
[INAUDIBLE] column, and different events. So to calculate whether the
user has passed the first item, we can define a
showButton variable. We use Lazy columns,
LazyListState sta and check if firstVisibleItemIndex
is larger than zero. And this is an
[INAUDIBLE] implementation that gets the
information that we need. The rest of the code
should be familiar. The first thing we want to
do is wrap this code-- a bit of refactor here. Wrap this code in
a box so our button shows on top of the Explore
section as we scroll. And then we add a
simple if statement. So if showButton is
true, we are going to show the
FloatingActionButton-- else, we hide it. Now, in order to
scroll to an item, we need to implement onClick
of our FloatingActionButton. And to do this, we can
just call LazyListState now that we have it-- .scrollToIteam. But again, this is
a Suspend function. So we need a coroutineScope. And we already know
how to solve this. We can define a coroutineScope
and use this coroutineScope to launch a coroutine. And we use it to run our
Suspend function, like so. And that's pretty much
all of the codes done. Now, if we run the
app, we go to a list of suggested destinations. And when we scroll, we
should see our button. It shows up whenever we
scroll past the first item. We tap on the button. We go to the top of the list. MANUEL VICENTE VIVO: That
button is actually very handy. The implementation works. But I think we can
make it more efficient. Right now, the composable
scope [INAUDIBLE] showButton recomposes as often
as first visible item index, which happens
frequently when scrolling. ALEJANDRA STAMATO:
Yeah, and also notice that Android
Studio complains as well. There is an error saying that
frequently changing states should not be directly read
in a composable function. MANUEL VICENTE VIVO: Exactly. Instead, we want
the composable scope to recompose only when
the condition toggles between true and
false or vise versa. There is an API that
allows us to do this, the derivedStateOf API. ALEJANDRA STAMATO:
The derivedStateOf is used when you want
to compose state that's derived from another state. The derivedStateOf
calculation block is executed every time the
internal state changes. But the composable function
will recompose only when the result of the
calculation changes. This minimizes the
amount of time functions reading showButton recomposes. MANUEL VICENTE VIVO:
ListState is composed state. And our calculation also
needs to be composed state because we want
the UI to recompose when its value changes to
show or hide the button. ALEJANDRA STAMATO: Using the
derivedStateOf API in this case is a better, more
efficient alternative. But [INAUDIBLE] is still
giving us a warning. And it's saying, creating a
state object during composition without using Remember. So we still have one
last change to make. And this is using Remember
together with derivedStateOf. So the calculated value
survives recomposition. And this is very
similar to what we do when we use mutable state
together with remember. Now we're all done. Our code works. And it's super efficient. Make sure you check
out the, When Should I use derivedStateOf blog post
linked in the video description to learn more about one
of my favorite APIs. [MUSIC PLAYING] So you did it. You've completed the advanced
state and side-effects in Jetpack Compose workshop. MANUEL VICENTE VIVO: Well done! Today, you learned
complex concepts about how to produce
UI state and how to consume it from the UI. You also learned
what side-effects are and the different
APIs that you can use. ALEJANDRA STAMATO:
A lot of APIs. Let's do a quick recap. MANUEL VICENTE VIVO:
Use LaunchedEffect to call Suspend functions
safely in the composition. rememberUpdatedState
to guarantee the usage of the latest lambda
value plus the subparameter in a side-effect. And rememberCoroutineScope
to launch coroutine scope to the composition outside the
scope of a composable function. ALEJANDRA STAMATO: produceState
to produce Compose data asynchronously in
the composition and derivedStateOf to
derive Compose state from another state efficiently. Two more takeaways to
remember before we leave-- if you're using a Lambda
inside a side-effect, consider wrapping it inside the
rememberUpdatedState guarantee the latest pass
value is always used. MANUEL VICENTE
VIVO: And if you are using objects inside
a side-effect, consider having them as keys
to restart the side-effect whenever one of those
objects changes. Strong emphasis and consider-- stop for a second. And think about it. Think if you want to
restart the effect or not. It might be the case
or it might not. Hashtag, it depends. ALEJANDRA STAMATO: After
you're done with the codelab, you can check out the
code in the end branch to compare your solutions and
see if you missed anything. Also, we recommend
taking a look at now in Android, our sample app that
showcases multiple of the best practices we saw here today. All the important links of
APIs, documentations, and blocks are attached in the
video description. MANUEL VICENTE VIVO: Thanks
for watching this workshop. And please let us
in the comments if you found the
guide useful, and also what you would like
to know more about. Thanks, and have a good one. Bye! [MUSIC PLAYING]