[MUSIC PLAYING]
MANUEL VICENTE VIVO: Hi, everyone.
Welcome to the third episode of the Architecture MAD Skills
series.
After learning about the data and UI layers,
now it is time to take a look at how to handle UI events.
Note that we will show Jetpack Compose code in this episode.
However, the same principles can be applied to views.
UI events are actions that should
be handled in the UI layer.
We'll take a look at two types of events,
the user and ViewModel events.
User events are produced by users
when they interact with the app, for example, by tapping
a button on the screen.
ViewModel events are actions that we
want the UI to take that originate from the ViewModel.
OK, let's start by covering user events.
As with previous episode, we'll use
the Jetpack Compose get new sample to show some use cases.
As I mentioned earlier, user events
are produced by the user interacting with the app.
For example, when there are no posts
available to show on the screen, we
show a button that lets the user load the content manually.
This code path can be found inside the HomeScreenWithList
composable in the HomeScreens file.
In Compose, we just pass a value to the onClick parameter
available in the button composable.
Note that in the view system, you
could use a certain click listener in the button
to react to the event.
You may already know Compose uses functions
as a way to propagate events, update your hierarchy until we
reach a composable that knows how to handle them.
As refreshing content is business logic,
we delegate that logic to the ViewModel.
Going up the UI hierarchy, we end up here
in the HomeRoute composable present in the HomeRoute file.
In there, we have a state for composable
where we can call our new model.
In the onRefreshPost's lambda, we
call homeViewModel.refreshPosts function that will
trigger the business logic.
If we take a look at the ViewModel class,
you can see how it exposes functions
that handle business logic for the UI to call when needed.
User events are very common in apps,
as it is what makes them responsive-- nothing new here.
You've probably been doing this for a long time already.
Let's move on to ViewModel events.
ViewModel events are actions you want
the UI to take that originate from the ViewModel class,
for example, telling the user that there
is no internet connection or that a request failed.
Even though some of these actions
produce transient UI updates, such as showing messages
on the screen, they should also be modeled
as part of the UI state.
These events should always result in a UI state update,
since that's the way we communicate with the UI.
And that is what we are going to do next.
Let's see how we can communicate with the UI
to display an error message on the screen
when a request fails.
Back to the HomeViewModel class, see
how HomeUiState models the UI state on the home screen.
It is exposed StateFlow, which is an observable data holder
type.
As alternatives, you could use live data or composite state
APIs, such as mutable state of.
As all ViewModel events should be modeled as part of the UI
state, we add a property to our home UI state
called errorMessages of type list of integers, which
represent a string resource ID, which
is a list, as we might want to queue up
multiple messages that are displayed sequentially
to the user.
Note that the ViewModel is agnostic of the UI
implementation.
These error messages could be displayed in a toast, snack
bar, or dialog.
The name is generic enough to accommodate any UI needs.
In this case, our designer decided
that we should show these messages in a snack bar,
as seen here.
Also, see how we allow the user to retry again,
if they want to.
Before jumping into the code, let's see
how this is going to work.
When the request fails, the ViewModel
will update the UI state with a new error message.
Then the UI will react to the new UI state
and display the message on the screen.
When the message is gone, either because the user interacted
with it or it timed out, the UI will
notify the ViewModel, which will remove the message from the UI
state.
In Compose the scaffoldState has a property
called snackbarhostState, which has
a method called showSnackbar.
We will use this method to show the snackbar.
The code we need to look at is the refreshPosts function
from the ViewModel that gets called
when new data is requested.
Even though we are interested in the case when
the request fails, let's take a quick look at the happy path.
When we call the function, we update
the UI state to indicate to the UI that the data is loading.
It refers to the current UI state, which is immutable,
so we make a copy of it and update it is loading.
Then we launch a new coroutine to call
the suspended getPostsFeed method from the repository.
If the result is successful, we update the UI state
with the postsFeed, which is the data that the repository
returned, and set the loading flag to false,
because the data is already loaded.
OK, that's cool, but we are in fact
interested in the case when the request fails
and the repository returns an error.
If that's the case, we need to update the error messages
queue.
First, we add the string resource ID of the load error
message to the current error messages queue
and then we update the UI state with a copy
that contains the new messages and the loading flag to false.
To show the snackbar in the UI, we
need to handle these error messages in the home screen
with the list composable, where we have access
to the snackbar home state, which
is part of the scaffold state.
Apart from the UI state and scaffold state,
the composable also takes two functions
as parameters, onRefreshPosts, in case
the user taps on the Retry button,
and onErrorDismiss, to notify when the error
message has been dismissed.
In the composable function body, we
need to check if there are any error messages
to display on the screen.
If that's the case, we retrieve from resources
the actual text for the error message and the one
for the Retry button.
To show the snackbar, we need to call the suspend show snackbar
function available in the snackbar state.
Since show snack bar is a suspended function,
we need to call it in the context of a routine.
LaunchedEffect is a composable designed
for exactly this purpose.
We call the function, passing our error message.
Then we draw a message on the scaffold state.
These are used as keys by the launch effect.
And if any of their values change,
the routine will be canceled and restarted
so that the UI always displays the most up
to date information.
As this effect creates a new coroutine,
we can now call the suspended showSnackbar function,
passing the error and retry messages as parameters.
Calling this function is what displays
the snackbar on the screen.
As any other suspended function, it suspends the coroutine
until the snackbar result is available,
which is either when the user taps Retry or the snackbar
is dismissed after a certain amount of time.
We can check whether the user tapped on the Retry button
by checking the snackbar result. If the result is
equal to action performed, the user tapped on the Retry button
and we called the onRefreshPosts lambda.
At this point, we know that the message is no longer
shown on the screen.
Therefore, we called the onErrorDismiss lambda
with the message I need to notify
that the error was dismissed.
Cool.
Our job here is done.
Snackbar will be shown whenever the ViewModel adds
a new message to the error messages list.
Up in our [INAUDIBLE] hierarchy, we
end up in the HomeRoute composable
that is in charge of interacting with the ViewModel.
Here, we give a value to the onErrorDismiss lambda
that tells the ViewModel that the error has
been shown to the user.
If we jump to that function, you can
see how we are removing the message from the error messages
list and updating the UI state with the new value.
The user interacting with the message or the message timing
out is a state change that the ViewModel should be aware of.
That's why the UI has to communicate when
it happens to the ViewModel.
However, this configuration is only
required for those events that are affected by user actions.
And that's it for this episode.
I hope you learned the different types of UI events and the best
practices to deal with them in code.
To learn more about this topic, don't
forget to check out our guidance on UI events.
Link below.
Next up in the series is the domain layer episode.
Don't forget to subscribe to the Android Developers YouTube
channel to get notified when it comes out.
Bye.
[MUSIC PLAYING]