[MUSIC PLAYING] MANUEL VICENTE
VIVO: Hi, everyone. I'm Manuel Vivo, part of the
Android Developer Relations team. When an activity or app is
destroyed and recreated, you must restore
the UI state quickly to provide a good
user experience. In most of these cases, the
user expects the UI state to remain the same. In this talk, we'll cover
how your app can lose state and how to avoid it by providing
best practices for saving your state. We'll also see how some
solutions work under the hood and how you can
apply that knowledge to solve advanced use cases. And lastly, a recap to make
sure you didn't miss anything. So how can an app lose UI state? Firstly, through
configuration changes. Some device
configurations can change while the app is running as a
device rotates, resizes, enters and leaves multi-window
mode, or the user switches to light or dark mode. When the configuration changes,
by default, the activity will be recreated
and initialized with a new configuration. But even if you can
avoid activity recreation in some cases in
a manifest file, it is impossible to completely
opt out of activity recreation. Some configuration changes will
always recreate the activity. To learn more about
this and why it happens, check out the Handling
Configuration Changes documentation. By the way, the app
you see on the screen, that's the Now in Android app. It's open source, and
you can take a look at the code on GitHub. Another way to lose
app state is when the system needs resources, and
your app is in the background. If the system is
low on resources, it will do its best to keep
your app processing memory. However, that's not guaranteed. The system may destroy
it while the user is away interacting with other apps. And, boom, it's gone. Lastly, the user or the system
can destroy your application abruptly. The user can swipe your app
off in the resident screen, can force quit your app. The app might be updated
in the background. So many different
things can happen. For each of these
scenarios, there are ways to save your state. Let's dive into them. As I mentioned earlier, it is
impossible to completely opt out of activity recreation. So you need to ensure that your
app can react appropriately to configuration changes. You cannot just lock your
orientation to portrait and hope for the best. OK, but I like that. To make data survive conflict
changes, use the ViewModel API. ViewModel instances
are cached in memory when the owner goes through
a configuration change. This makes the ViewModel's data
remain intact after the config change happens. ViewModels are only limited
by the available memory in the device, and
reads and writes to a memory state in
ViewModels are fast. In addition to this,
the navigation library also caches ViewModels
when the destination is kept in the back stack,
which is a nice touch, given that the data will be instantly
available when the user goes back to the destination. Because all of this, ViewModels
are the recommended solution for making your state
survive conflict changes. You can see an example of
that in this code snippet. We have a ViewModel
as a stakeholder for a particular
screen that exposes screen UI state to the UI. The screen UI state is
built with information from other layers
of the hierarchy and will still be there
after a config change. Note that in the
previous code snippet, we were using ViewModels
for two purposes-- first, as a mechanism
to make data survive config
changes and, second, as a screen-level state holder
that exposes data to the UI. If you don't use ViewModel as
a state holder, that's fine. But consider using
ViewModel under the hood to make data survive
config changes. The ViewModel API is
the only supported way to make large and arbitrary
objects survive configuration changes on Android in the
scope of an activity, fragment, or a navigation destination. Before diving into the
system needing resources, let's cover unexpected
app dismissals first. It is a more common use case. And it's going to help us
understand the other use case better. For data to survive
unexpected app dismissals, we have to use a completely
different approach. Instead of memory, we need
to persist our information to disk. Persisting the data over the
network in your own servers is also another option. But in this talk, we are just
covering persisting locally on the device. For that, we have two APIs
available in Jetpack, DataStore and Room. DataStore is ideal for
small or simple data sets. And you should consider Room
if your data is well structured and has needs for partial
updates, referential integrity, or is a large or
complex data set. Persistent storage also survives
conflict changes and the system needing resources. It stores the
information on disk, making the solution
limited by disk space. And because it requires I/O
operations, it has slow reads and writes. You would usually store
application data on disk. Due to how slow the solution
is, it wouldn't make sense to generally store UI state. UI state is dynamic. It can change quite often. If you stored UI state
directly on disk, your app could be slow to
respond to changes in the UI. However, application
data is totally dependent on the business
requirements of your app. For some apps, some
UI states might be considered application data. The last area we
need to cover is when the system needs resources. In this case, the system might
be in a critical situation, and it might kill your process. Then it will recreate it
at some point in the future when the user goes
back to your app again. We don't have a problem
with application data because it's persisted on disk. However, UI state
is in memory, and we are going to lose it all. Luckily, to not completely
damage the user experience, Android provides a mechanism
to save essential data so that the user can
return to the state they were in before the
process was recreated. This solution is the
saved state APIs, which rely on Android bundled
objects under the hood. There are APIs for Jetpack
Compose, the view system, and ViewModels. We'll look at each
of them later. The system persists
saved state bundles through both
configuration changes and when the system
needs resources. The bundle's stored in memory. Android keeps a serialized copy
of the data in memory outside of your process. The size of the
bundle is limited, so use it to store a minimal
amount of data necessary. Trying to store
large objects could lead to runtime exceptions. We recommend that you don't
store more than 50 kilobytes. Due to the need of serialization
and deserialization, the read and write
times could be slow. The time depends on the
complexity of their types and the size of the bundle. The system might even try
to optimize this and leave the same bundle object in
memory without serialization for quicker access. But these behaviors might change
across Android API versions. So please, don't store
large objects or lists. Serialization could
consume a lot of memory if the objects being
serialized are complex and this process happens
on the main thread. Remember that the system might
be in a critical situation. Usually, data
stored in safe state is transient state that depends
on user input or navigation. Examples of this can
be the scroll position of at list, the ID of the
item the user wants more detail about, the in-progress
selection of user preferences, or input in text fields. There are different APIs
for each UI toolkit. If you are using Compose,
use the rememberSaveable API, and in the view system, the
onSaveInstanceState callback. You could use these
APIs when the state is needed by UI logic-- for example, when
tracking whether or not a UI element is expanded. The Compose code we have on the
screen is for a text message. Tapping on the text shows
or hides more details. The show details variable
uses rememberSaveable. This makes it survive
configuration changes and when the system
needs resources. In the View system,
you could have a custom view like
the following with the same isExpanded Boolean. To save state, you would
overwrite the unsaved instance state method,
returning a bundle. To restore state,
you get the bundle from the
onRestoreInstanceState method. Something to watch out
for is that the view must have the isSaveEnabled
property set to true, and it needs to
have a unique ID. To create automated tests
for these behaviors, you can use the
StateRestorationTester in Compose and the
ActivityScenario.recreate function in the View system. In the following code, we
are testing the Compose code we showed earlier using
the Create Compose rule function and different
Compose testing APIs. See how we are creating
a state restoration test instance passing in
the Compose test rule and how we use the
emulate saved instance state to restore function to
test that remembered saveable behavior. These APIs makes sense when
state is part of the UI logic because your state
is present in the UI. However, when state is
needed by business logic, your state will likely be
present in screen-level state holders. If you are using ViewModels
as a state holder to handle the business
logic complexity of the UI, you have to use the
SavedStateHandle API instead to contribute to saved state. Here, we can see
a ViewModel that holds the message
the user is currently typing in a conversation. It's using the
saveable function, which is SavedStateHandle's
integration with Compose state. SavedStateHandle also provides
integrations with other streams of data, like state flow. But something to
keep in mind when working with SavedStateHandle
is that it only saves data returned to it when the
activity is stopped. If you update it when the
app is in the background, the system will store
the data the next time the activity is stopped. For more information
about SavedStateHandle, check out the documentation. As a summary, here is a
table of the saved state APIs we recommend, depending
on the type of logic that you applied to the data-- rememberSaveable or
onSaveInstanceState if you are using the
state for just UI logic or SavedStateHandle if you need
the data for business logic, and you are using ViewModels to
handle the screen complexity. If you don't use
ViewModels for that, don't skip the next
section of the talk. Cool. We are going to move to more
complicated use cases now. And for that, we need
to do a deep dive and see what's happening
under the hood. The first use case
we are covering is how to contribute to saved
state from your own classes. We've been presenting
the ViewModels of state holder implementation
for screens in your app. However, due to their
scope, ViewModels are not a good solution
for managing the complexity of reusable UI elements. Imagine that we have a reusable
search UI element for news, and we want to save the searched
user input into saved state. Our state holder in Compose
would look like this. We are passing the
news repository and the initial search
input as parameters. Then we have a mutable variable
with Compose's text field value. As we saw earlier, the way
to contribute the state to saved state in Compose is
with a rememberSaveable API. Following Compose
API conventions, we can create a
remember function that uses rememberSaveable
under the hood. It would look
something like this. RememberSaveable is going
to take a variable number of inputs that indicate when a
new state needs to be recreated with the new values. However, because NewsSearchState
is a complicated object, we need to provide
a custom saver. A saver describes how an
object can be simplified and be converted to something
that is saveable, which makes it eligible to
be stored in the saved state. Back to our
NewsSearchState, here is a saver implementation
for the class. A saver needs to provide two
functions, save and restore. Because our text field value
state has its own saver already, we can simply delegate
that functionality to it and save our current
search input. Same thing with
the restore LaMDA. We delegate that to the
saver and call restore(it). With the result, we
create a new instance of our NewsSearchState, passing
in the restored searchInput and the newsRepository
that we are passing to the saver function. In our rememberNewsSearchState
function, now we call the saver, passing
in the news repository. That wasn't too bad, was it? We are using
rememberSaveable to do it. How can we do the same
in the view system? Here, we have our
NewsSearchState holder, again, with the current
query as a string. We cannot use SavedStateHandle
because this class isn't extending ViewModel. Also, we cannot use
UnsafeInstanceState, because that's only
available in a view class. To better understand
the solution, we need to look at
the different APIs that saveState provides
in the view system. The SavedStateRegistry is
a nonspecific interface that allows components
to save and restore their state using the saved
instance state mechanism. Then you also have
providers that can contribute
content to saveState within a registry owner. So let's put this into practice. Back to our
NewsSearchState, if we wanted to save current
query into saveState, we would need to make
the class implement the SavedStateProvider
interface, then implement the
saveState method that is called before the
registry owner is stopped. In there, we save our state
into a bundle that we'll return. Now we have to connect
this with a registry owner that we pass as a parameter
in the constructor. At init time, we registered
the search state as a provider in the owner SavedStateRegistry. And then we can
restore the state if it was previously
saved by calling the consumeRestoredStateForKey
method. And that's it. We can save and restore our
mutable state from saveState. If you're using the search
UI element in a fragment, then you would initialize
the state holder like this. And that would be it. That's how we can
contribute to saved state from our own classes in
Compose and in the View system. Now we can move on to
another advanced use case, how to control the lifecycle
of rememberSaveableValues. Yeah, you can control this. By default, if
rememberSaveableValues will be restored as
long as the UI element was in the composition before
the save event happened. If you remember the
composable lifecycle diagram from our docs, a composable
enters the composition, can recompose zero
or more times, and finally leaves
the composition. What we said means that when
the UI enters the composition, the rememberSaveableValues
are stored in saveState. Now, if a configuration change
happens and the activity is recreated, the old
composition is destroyed, a new composition is created,
and rememberSaveableValues are restored. Note that rememberSaveableValues
are restored, but values using the remember
API once, they are lost after the
activity is recreated. And then, lastly, when
the composable finally lifts the composition, the
values inside rememberSaveable are removed from saved state. Let's see how we can modify
this default behavior. We've seen the View system
APIs involved in saved state. If we draw a parallel line
and talk about Compose APIs, we're going to find a
lot of similarities. We have the saveable
state registry interface that allows components to
save and restore the state. One big difference here
is that this interface is platform agnostic. It is not specific to Android. When Compose runs on
an Android target, SaveableStateRegistry
is connected with SavedStateRegistry via
the disposable saveable state registry implementation. Then we have saveable
state holders that can control
how to contribute content to SavedState with
a SaveableStateProvider. In Compose, you can create
instances of these APIs with a
rememberSaveableStateHolder function. And this is exactly
what you need if you want to
control the lifecycle of rememberSaveable values
in a particular part of the composition. Looking at that
rememberSaveable implementation, it accesses the current
SaveableState registry, and it gets initialized
by calling consumeRestored from it. If there was no value
previously stored, it gets initialized
with the init LaMDA. So if we define a new
SaveableStateRegistry, we can control for how
long rememberSaveable stores their values. And this is precisely what
navigation Compose does. Navigation, apart from
caching ViewModel instances when the destination is
part of the back stack, it also keeps in memory
the rememberSaveable values of those destinations as well. Let's see how we are doing it. On the screen, we have the
NavHost composable function, which declares a new
SaveableState registry and state holder using that
rememberSaveableStateHolder API. Here, we have the API in action. Then the content of a
particular destination is placed inside the local
owner's provider that is called on the current backStackEntry. LocalOwnersProvider has
some composition locals and calls a custom
SaveableStateProvider. This SaveableStateProvider
controls for how long the
rememberSaveableValues are going to be kept
in the registry. In this case, if you look at
the implementation details, it's going to keep them as
long as the destination is in the back stack. Let's see an example of that. Here we are on the Interest
screen of the Now in Android app. The bottom bar
navigation has three tabs that we have in the back stack. See how its destination
has a different ID that is connected with
the navigation's SaveableStateHolder. On the interest screen,
when we tap on an interest to see more details of
it, a new screen opens, and the new destination is
added to the back stack. Now, if we go back to
the previous screen, the destination will be
removed from the back stack. When this happens, navigation
calls the removeState function with the corresponding ID
to remove all associated rememberSaveableState. Now, both the
destination and the state saved in its
SaveableStateHolder are gone. They are no longer in memory. We've seen an example
of how to control for how long
rememberSaveableValues remain in memory. If you happen to need
the same behavior for your particular use case,
rememberSaveableStateHolder is the API you need. Coming to an end, here
are the different ways you can lose up state. Remember that your activity
and process could recreate, and there is nothing
you can do to avoid it. We also looked at the
different solutions we have to mitigate this and
provide a good user experience. The ViewModel survives
configuration changes, SavedState, config changes plus
the system needing resources, and persistent storage, all of
the above plus unexpected app dismissals. They're limited by the
available memory, the bundle, and disk space. Use ViewModel to
hold UI state that needs to survive config changes
like screen UI state, saved state for transient UI state
that depends on user input or navigation, and persistent
storage for application data. At the top, we have
the fastest solution for a type of data
that changes more often and requires almost no delays to
provide a good user experience. At the bottom, the slowest and
most reliable solution to store application data
that cannot be lost. Which one to use? You might need none of
them or all of them. It depends on the necessities
of your UI and the state that it contains. As many other architectural
recommendations, treat this as guidelines
and adapt them to your requirements as needed. Thank you, everyone,
for watching, and hope to see you in
future architecture talks. Bye. [MUSIC PLAYING]