Tuesday, June 21, 2016

Declarative State Management in Android Activities

TL;DR: State management in android can be nasty. Check out the code links at the bottom that use annotations to make it better.

State management in Android is a challenge for all developers, novice to experienced. Recently I ran into a scenario where I needed to persist a non parcelable/serializeable object on orientation change. Woe was me, with onRetainNonConfigurationInstance() already persisting another object I didn't want to create a map to hold both of these objects. So how can we fix this problem?

For the uninitiated, lets go over the two mechanisms for persisting data in activities.

The first is through a bundle. In android you can save primitives and serializable/parcelable data into a bundle when your activity is being destroyed via the onSaveInstanceState() method. This can then be retrieved from the bundle in the onCreate() method of your activity. If you want to learn more about this approach you can check out the description in the developer docs.

The second approach is to use onRetainNonConfigurationInstance(). By overriding this method you can return an arbitrary object that you can then access in onCreate(). This stackoverflow post does short work of explaining how to use this approach.

Both of these approaches have their drawbacks. Using a bundle limits the type of data you can persist, while using onRetainNonConfigurationInstance() limits you to saving one object (with the added risk of overriding the method in a child class). In addition, both of these approaches require the developer to repeat code to manage saving and retrieving each piece of data.

I'm bored, give me your secrets Knave!

Okay, I got the message. What I needed was an approach for persisting data that would be concise and flexible, something that would be as simple as annotating fields that should be persisted. And with a little bit of structure and fiddling you can do exactly that.

First go ahead and create a new android application with an empty activity (go with the default name, MainActivity). We are going to put a Button and TextView in MainActivity. When we tap the button the value in the text view should change. After rotation we should expect the value in the view to remain the same.
Go ahead and run the project. You should see a screen pop up with a text view, and a button. Tap the button a few times to see the count change and then rotate the screen. Unfortunately the value will not be persisted and the value of the text view will return to 0.

So let's fix this! First, we will have to create a BaseActivity that all of your activities can extend. Sidenote: this is a good practice to follow in general.
In your BaseActivity, create a new annotation:

Now override onRetainNonConfigurationInstance() to find all the fields with the annotation in the object and save them to a map.

For those of you who have never written an annotation, we are using reflection to get access to all the fields in the activity object and then getting the values of the fields that are marked with our @Persistable annotation.

Great! So now we are saving our persisted data. The next step is to read back the values.

In onCreate we are going to get the data using getLastNonConfigurationInstance(), then we will use reflection again to set the values of any @Persistable fields.

At this point we need modify our MainActivity to work with our new persistence framework. All we need to do is change which activity we are extending to BaseActivity and add the @Persistable annotation to our count variable.

Go ahead and run the project. You should see a screen pop up with a text field, and a button. Tap the button a few times to see the count change and then rotate the screen. You should see the count remain unchanged.

Part 2

Before we wrap things up we are going to clean things up a bit.
First we are going to recursively get the fields for the object and it's super classes. Right now if we tried to persist an object in BaseActivity it would be ignored, this will address that problem. While we are at it we will make onRetainNonConfigurationInstance final so none of our subclasses override it.

Run the Project and check that it still works correctly.

Next we are going to clean up the logic in BaseActivity.onCreate() by moving the logic for reinitializing fields to another method.

Congratulations! You have just implemented declarative persistence! Share your thoughts about the approach in the comments.

Ugh, sure. Check out the full sample project here
Of special interest is the BaseActivity and the MainActivity

0 comments:

Post a Comment