Найти тему

Сохранение состояния пользовательского View в Android

Оглавление

Android save state in a custom View.

print_matrix.py

Как сохранить состояние в пользовательском представлении.

Присвойте ID вашему View

Android не будет сохранять состояние вашего представления, если экземпляру вашего представления не назначен идентификатор.

  • View создается из XML-файла
gist:b012f4a44a7b92473fb1d92999bc5277

  • View создается программно
gist:b012f4a44a7b92473fb1d92999bc5277

Сообщите Android, что вашему View необходимо сохранить состояние

Вызовите следующий метод в своем пользовательском представлении, чтобы ваше представление сохраняло свое состояние

gist:b012f4a44a7b92473fb1d92999bc5277

Реализация сохранения состояния

Теперь мы подошли к сути вопроса. Процесс сохранения и восстановления свойств в представлении включает в себя два обратных вызова представления и реализацию Parcelable. Parcelable отвечает за сериализацию и десериализацию свойств в определенном порядке. Обратные вызовы представления отвечают за передачу соответствующей информации в Parcelable при сохранении состояния, а затем соответствующее применение данных из Parcelable при восстановлении состояния.
Для этого примера предположим, что ваше пользовательское представление имеет строку с именем «имя» и целое число с именем «индекс». Вы хотите сохранить оба из них, а затем восстановить их.

Subclass BaseSavedState

First, you need to create an appropriate Parcelable. In practice, you’ll probably never implement Parcelable directly. Instead, subclass BaseSavedState and implement the necessary methods.

Typically, BaseSavedState is extended as a private static class within a custom View. We’ll take that approach here:

Next, let’s override the constructors, as required:

Notice that one of the constructors simply calls through to the super constructor, but the other constructor includes our own data reading logic.

Take a moment to appreciate the difference between the two constructors. At first they may appear similar. It is tempting to assume that a Parcel is similar to a Parcelable, but they’re actually very different in purpose. A Parcel is a package of data, like a box dropped at your door by UPS. A Parcelable is a thing that packages data, like Amazon who put the items in the box that was dropped at your door.

The constructor that takes a Parcelable is an implicit implementation of a composite structure. This constructor allows your state saving strategy to wrap around your super class’s state saving strategy, which wraps around your super class’s super class’s state saving strategy, all the way up the inheritance tree. This constructor is about setting up an appropriate structure to save state.

The constructor that takes a Parcel is providing you with actual data. This constructor is about restoring state.

Lastly, don’t forget that order matters. Parcelables are all about serializing data, which means the order that they’re written impacts the order that they need to be read. But what order are the properties written? Well, that brings us to the final method that we need to implement within a Parcelable:

It’s up to us the order that we read and write data, but it needs to be the same order between the two actions.

Finally, there is one last thing we need to do with our SavedState object, which is an expectation of all Parcelabes. We need to declare a static creator called “CREATOR”:

The existence of a static member called CREATOR is an expectation imposed by Android, so we must provide it.

onSaveInstanceState() and onRestoreInstanceState()

With a custom SavedState implementation in hand, its time to implement the View methods that use our SavedState: onSaveInstanceState() and onRestoreInstanceState().

Use the following code and comment instructions to implement state saving:

That’s it for saving state. To restore state, begin by overriding onRestoreIntanceState(). Follow the example and comment instructions below:

Do whatever you need to with these properties to configure your View as desired.

That’s it for saving and restoring custom View state. The rest of this post discusses nuances that may be of interest.

View state is NOT Activity/Fragment state

With all the state saving and restoring going on in Android, its easy to assume that the Bundle-based state saving in an Activity and Fragment are somehow tied to the View hierarchy. They’re not. This is a common point of confusion for beginner and even intermediate Android developers. They start poking around the Activity and Fragment APIs looking for the opportunity to save state for a custom View, but nothing seems to work. That’s because Activity/Fragment state is a completely different beast than View state.

Activity/Fragment state tends to be application state, e.g., business rules, navigation location and history, authentication status, etc.

View state, on the other hand, represents an exclusively visual configuration at a moment in time, e.g., scroll position, animation progress, or maybe a checkbox status (though, it’s debatable whether a checkbox status should be View state or application state).

The difference between Activity/Fragment state and View state is not merely semantics. Developers will notice that an Activity/Fragment utilize Bundles to read and write properties where Views use Parcelable implementations. While a Bundle is a Parcelable, a generic Parcelable is not a Bundle, so this technical difference makes the two processes API incompatible — they simply do different things in different ways.

When a developer is interested in the state of a View, the developer should look within the View hierarchy, itself. When a developer is interested in application state, the developer should look within an Activity/Fragment (or preferably, a developer should avoid Android components altogether when persisting valuable application data).

View, ViewGroup, and how Android saves state

If you’re an Android developer, it would be a good idea to inspect the actual source code for View state saving/restoring and understand what is actually taking place. Inspecting source code can be daunting for some developers, so I’ll provide an outline of what the call path looks like and provide links to the relevant source code. I personally got started down this path by reading this great article at Tricky Android.

  1. At the root of a visual application is a Window. More specifically, a PhoneWindow.
  2. A PhoneWindow contains a root View for all of its UI that it calls mContentParent.
  3. At the appropriate time, Android instructs the Window to saveHierarchyState(), which means “save the state of the UI”.
  4. PhoneWindow’s saveHierarchyState() is executed, which then invokes a method of the same name on its root View. From this point forward, we are in the normal View-based state saving process that you’re used to. But maybe you didn’t realize that it begins with saveHierarchyState().
  5. We don’t know what kind of View is at the root of the Window, but let’s assume for a moment that its just a basic View. In that case, when saveHierarchyState() is called, it then immediately invokes dispatchSaveInstanceState() on itself.
  6. The View’ dispatchSaveInstanceState() then does 2 things of interest. First, it invokes onSaveInstanceState() on itself (you should recognize that method). Second, it takes the Parcelable that it got from onSaveInstanceState and sticks it in a SparseSarray called container, which was provided to it by PhoneWindow.
  7. Then PhoneWindow takes the returned SparseArray and sticks it in a Bundle, then PhoneWindow returns the Bundle to the Android system.

The above details provide two benefits. First, we now know where View state saving begins and where it ends, i.e., we know the boundary of the system in question. Second, this investigation reveals that we should pay very close attention to saveHierarchyState(), dispatchSaveInstanceState(), and onSaveInstanceState(), wherever we see them.

Looking further into the source code reveals that ViewGroup alters this behavior a little bit. ViewGroup re-implements dispatchSaveInstanceState() so that it also gets invoked on all of its children. ViewGroup also introduces a method called dispatchFreezeSelfOnly() so that a ViewGroup subclass can choose to save its own state without its children.

Knowledge of these implementations should aid you greatly in saving and restoring state for your customer Views.

There is still one problem that is revealed by this investigation that we haven’t solved. That is the problem of a custom ViewGroup saving the state of its children. More specifically, it’s the problem of a custom ViewGroup that creates some number of its own children, rather than receiving those children from the app developer.

Imagine that a custom ViewGroup shows a vertical list of provided items, but the ViewGroup renders its own TextView title at the top of the drawable area. How will the state of the TextView be saved?

We know that an ID is required to save state, so we can just give the TextView an ID and everything will just work, right? Well, everything will “just work” so long as there is only one instance of that custom ViewGroup in the View hierarchy at any given time. If there are ever two or more instances of that ViewGroup, then each ViewGroup will end up overwriting the TextView’s state because they all gave the TextView the same ID. How do we know this? Because when we looked at the source code, we saw that all of this state saving was being done with a single SparseArray, whose keys are the IDs of each View. This means that two or more Views with the same ID will collide in the SparseArray and mess up the state saving system. So what do we do?

I can think of 3 possible ways to resolve this issue. I’m not sure which of them is considered canonical. I haven’t looked for any such examples in source code.

Create an ID registration system

The fundamental problem is colliding IDs. If the IDs don’t collide, then everything should be fine. Therefore, you could probably create some kind of static ID factory method that gives a unique ID to every instance of your custom View. That would prevent all of your inner TextViews from colliding. Of course, this now opens the door to the possibility of colliding with completely unrelated Views. I’m not sure how to solve that.

Manually save state of explicit child Views

Rather than ask the TextView to save its state into the SparseArray, your custom ViewGroup could instead extract the desired state from the TextView and save it directly. For example, you could call myTextView.getText().toString() and save that String to the ViewGroup’s state directly.

Create a new SparseArray and give it to explicit child Views

A SparseArray can be given any object as a value, even another SparseArray. Therefore, your custom ViewGroup could override dispatchSaveInstanceState(), create a new SparseArray, save your custom ViewGroup state and all children’s state to the new SparseArray, and then store that SparseArray in the original SparseArray as your custom View’s state.

In other words, you could go from this:

To this:

And with that I’ll get back to the actual Android work that made me rediscover all of this in the first place. Here’s to hoping I don’t need this information again for another year!