Beating the Android life cycle

Julius Huijnk
8 min readDec 22, 2016

--

I’d like to share my blueprint for dealing with one of the fundamental challenges when developing for Android once and for all.

Update May 2017:
Perhaps it will get easier with Google’s new
Lifecycle Components.

The scenarios

These are the scenarios that must be covered make the activity work and sustain its state when expected.

  1. First time loading
  2. Losing focus
  3. Rotation
  4. Save
  5. Leave
  6. Return via alternative route
  7. Return after application process is killed

How to deal with them

On the one hand you can make use of savedInstanceState, this is the official route and is in many cases the simplest solution. I’ve chosen to work around it, because I’m dealing with an activity with custom views and those views don’t have unique id’s.

My solution consists of a controller that is responsible for keeping state. The graphic below is what I call ‘the blue print’ and it shows the communication and basic logic to beat the Android life cycle.

Below, I’ll explain more about the back story, use case, reasoning, and the scenarios that are covered.

Back story

I made an app a couple of years ago that’s a simple diary / goal setter. It’s free and about 500 people used it last month. Some of these users where asking for more than 4 categories to write their measurements in. The code was super deterministic and not so robust, so I decided to rewrite most of the app and share with you my documentation for dealing with the life cycle.

Use case: Writing a measurement

One of the main features of the app is adding a measurement.

For each category, it contains:

  • Five styled radio buttons to provide a score.
  • Input fields that show when you score a category.

I’ve put these in a custom view group, because I re-use them, and wanted to learn how to make use of them. Those views need to be created when starting the activity and are not part of the layout. While tempting, just using a android:configChanges = "orientation" in the layout won’t cover everything and I wanted to solve this issue once and for all, so I don’t have to think about it in the future.

What we are trying to beat

As a reminder, this is the life cycle for an Activity.

Activity life cycle from Android documentation

The design principles

When you use savedInstanceState you can manage all the details by hand, or make use of IcePick to help you out. With custom views it gets mor complicated, here one on how you can use saved instance state with custom views. It’s rather complex, and since my custom views did not have unique Id’s, I began to explore a different route. Also I felt that the activity / fragment should be responsible for storing their own state and I wanted a generic way that would be 100% robust independent of what type of views would be used.

My design principles:

  • Use the activity only to present and gather input
  • Everything about the state is stored in a controller class.
    I’m using a singleton class in my app.
  • Communication is done via events.
    I’m using EventBus for communication. Eventually I want to put the controller in a service, and using events makes moving the controller easy.

Scenarios

To keep the article short and generic, I’ll write methods you have to implement yourself. For example a fireClearEvent() would hold EventBus.getDefault().post(new ClearEvent());.

1. First time loading

When the user enters the activity, the custom need to be created. We need the activity to go get the category information from the controller where it is loaded from the database.

At onResume():

The noCustomViewsCreated has initial value of true and tracks if the custom fields have been created. The InitEvent is fired and picked up in the controller.

The controller has fields that hold the state of the app; for instance the id of the measurement and the content (score and text) itself. At this moment fieldsEmpty() should return true. I have created a class to hold the content of each custom view and it’s corresponding categoryId, so I can store an ArrayList of those items. The controllerSharedPrefsEmpty is needed for scenario 7.

At createContent(mode, id) the ArrayList is filled with content. After that the ProvideEvent is sent and picked up in the activity.

You can then get the values from an event using event.viewsContent, or whatever was passed. Here the onProvideEvent(..):

if(noCustomViewsCreated){
createViews(event.viewsContent);
noCustomViews = false;
}
addContentToViews(event.userContent);

The noCustomViews is still true, so createViews(event.viewsContent) is called. Here your custom views will be created. Then the flag is set to false, making sure that the next time that onResume() triggers this method, it won’t recreate all the views.

After that the addContentToViews(event.userContent) populates the view with content. When first starting, and in edit-mode, this might contain user content.

2. Losing focus

The app is minimized, or another app gains focus, like when you get a phone call. Here you risk losing the user input. The onPause() will be called when leaving and onResume() when returning.

The user might have provided input, so the activity does not have to be in sync with the controller. So at onPause() we fire an StoreEvent(), this holds all the content of the views. In the controller this content will be stored in the fields.

At user returns at onResume(). Now noCustomViewsCreated is false and the RequestEvent is fired. This triggers the controller to directly return a ProvideContentEvent that passes the content that was available to the activity where it is used to populate the custom views.

3. Rotation

On rotations, the whole activity gets recreated. So the risk is to lose all the state of the activity. For me it was not directly clear how to distinguish between leaving the app for real and leaving the app for rotating. I found this post explaining that isFinished(), part of Android api, can be used to make that distinction.

This is is the flow that is used:

Like when losing focus we first store the content of the activity in the controller. Then at onStop() the isFinishing() will be false, so nothing else happens.

When the activity is recreated in a different orientation, the onResume() will trigger the same events as when regaining focus.

4. Save

When saving, we now want to store the content in the database and make sure that the next time we visit this activity it will be empty.

A SaveEvent() is passes the content to the controller, where it will be saved to the database. Then a ToNextEvent() is sent to trigger the activity to do the following:

@Subscribe(threadMode = ThreadMode.MAIN)
public void onToNextEvent(ToNextEvent event) {
Intent intent = new Intent(this, NextActivity.class);
intent.putExtra(MyActivity.KEY_MEASURE_ID, event.measureId);
startActivity(intent);

finish(); // removes activity from stack
}

With this design, it’s important to not forget to call finish() when going to the next activity. This will trigger the onStop(), now with a true for isFinishing(). So when leaving to go to the next activity the ClearEvent is called bringing the controller back to the state it was before the user entered the activity.

5. Leaving the activity for real

Users can leave the app via a back-button or up-button. Make sure that when activity is not left on the activity stack and onStop() will be called. I do this in for the up-button in the onOptionsItemSelect(..), by calling finish() in the case of android.R.id.home.

6. Return via alternative rout

You can leave the activity out of focus and come back to the same activity via a different route, like via a notification or widget. I haven’t tested this, but I believe my design would hold up. Since the controller is holding on to the state until it is explicitly cleared.

7. Return after the application process is killed

When the app goes out of focus and the device is low on memory, your application process can be killed. This will also remove our controller instance and thus wipe the fields clear.

This means we need three extra steps:

  1. At the store() we need to also store our content persistently. I’ve chosen to use the shared preferences. In order to save the ArrayList with custom objects, I first convert it to JSON and then to a String.
    You could use GSON, but I’ve chosen to use JSONObject and JSONArray so I have one less dependency in my app. When unit testing with JSONObject, make sure to add it to your testCompile dependencies.
  2. At the clear() we need to clear the shared preference that was set at store().
  3. At the onInit() we recognize we are in this scenario ,because the fields are empty but the shared preference value is not. We then must ‘unpack’ our shared preference String, store the ArrayList to the field in the controller and pass it to the activity via an event.

Does it work?

I have done many tests to confirm this works, but not all automated. I have not figured out a way to test rotation, going out of focus, or killing and restoring the application context in an automated way for apps like mine that support Android 2.3.

The controller doesn’t even know if it is holding information for an activity or a fragment, so it’s not hard to use the same basic design on a fragment. Be sure to put the code for onCreate() inside onCreateView() for a fragment. For all else, I believe it works. Don’t take my word for it, do enough testing so you know.

Other ways?

So there you have it, there is at least one way to beat the Android life cycle. I started by stating that you can also make use of the savedInstanceState. In your case, this might be a much simpler solution, so you may want to try that first.

If you’re checking out the personal goals app, also check out my free Idea Growr app (1000+ reviews score it 4.2 out of 5).

And if you’re into UX, you might like to read my article on UX tools.

Have a nice day!

I’d like to thank Christophe B. for pointing me to what has become scenario 7 and providing extra feedback on Google+.

--

--