Android Activity Life-Cycle for Professional Developers

By | 2018-05-25T10:51:26+00:00 April 11th, 2018|Android development|10 Comments

Your eyes do not lie – it is 2018 and I’m writing an article about the life-cycle of Activities in Android. Believe me, I’m surprised myself.

See, I thought that Activity life-cycle, as overly-complex as it is, is not an issue anymore. I mean, I knew that new Android developers struggle with it enormously, but I couldn’t even imagine that it is still a thing among experienced Android professionals.

Naive me.

I will tell you the whole story in a moment, but just let me state the goal of this post in general.

I’m going to share my way of handling of Activity life-cycle. It is much simpler than what you’ll find in official documentation and tutorials, and covers the absolute majority of tricky edge cases.

Activity life-cycle is still an issue:

Two events that happened over the course of a single week opened my eyes to the fact that even today Activity life-cycle is still an issue among Android developers.

A redditor shared an article about Activity life-cycle on “androiddev” sub-Reddit couple of weeks ago. This article was based on my older StackOverflow answer and I immediately noticed that one of the practices recommended by the author was incorrect. I left a comment to let other readers know about that.

Several redditors replied and challenged my views which led to a long and very insightful discussion. During this discussion we found out several interesting facts about Activity life-cycle:

  1. Activity life-cycle confuses even experienced developers
  2. Official documentation still contains outdated information and contradictions about Activity life-cycle
  3. Even developers who write official Google tutorials can lack understanding of Activity life-cycle and its implications

Few days later I was visiting a potential client. This company employs a team of mature Android developers who are trying to tame a legacy codebase while delivering new features. Not an easy task at all.

During a quick code review I noticed that one of the previous maintainers decided to subscribe Activities to EventBus in onCreate(Bundle) and unsubscribe in onDestroy(). Doing this is a sure recipe for troubles, so I recommended them to refactor this implementation.

My take on Activity life-cycle:

The above two events made me realize how naive I was believing that Activity life-cycle is not an issue anymore. Now, that I know how much confusion about it still exists, I would like to try and make it easier for other developers by sharing my experience.

I don’t intend to write an alternative documentation for Activity life-cycle here. This would be too big of a task for a questionable gain. Instead I will show you how to partition logic between life-cycle methods to achieve the simplest design and avoid the most common pitfalls.

To make things simpler, I will assume that you are already familiar with Activity life-cycle in general.

onCreate(Bundle):

Android framework constructs all Activities by itself. So, no constructors for your Activities. Alright, but where do you initialize Activity then?

You do it in onCreate(Bundle) method. This method should host all the logic that would reside in a constructor otherwise.

Ideally, a constructor will just initialize object’s member fields with injected dependencies:

    public ObjectWithIdealConstructor(FirstDependency firstDependency, 
                                      SecondDependency secondDependency) {
        mFirstDependency = firstDependency;
        mSecondDependency = secondDependency;
    }

[To better understand why the above constructor is ideal read this article by Misko Hevery. Even though Misko discusses the issues associated with “constructors that do too much” in context of unit testing, these constructors are equally bad in context of code readability and maintenance.]

In addition to member fields initialization, onCreate(Bundle) has two more responsibilities courtesy of Android framework itself: restore saved state and call setContentView().

So, the general functionality inside onCreate(Bundle) method should be:

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getInjector().inject(this); // inject dependencies

        setContentView(R.layout.some_layout);
        
        mSomeView = findViewById(R.id.some_view);

        if (savedInstanceState != null) {
            mWelcomeDialogHasAlreadyBeenShown = savedInstanceState.getBoolean(SAVED_STATE_WELCOME_DIALOG_SHOWN);
        }

    }

Evidently, one size doesn’t fit all and you might have a bit different structure. That’s fine, as long as you don’t have the following in onCreate(Bundle):

  • Subscription to observables
  • Functional flows
  • Initialization of asynchronous functional flows
  • Resources allocations

Whenever I need to decide whether a piece of logic belongs in onCreate(Bundle), I ask myself this question: is this logic related to initialization of this object? If the answer is negative, I find another home for it.

After Activity had been instantiated by the framework and its onCreate(Bundle) method run to completion it will be in “created” state.

“Created” Activity shouldn’t cause allocation of resources and shouldn’t receive events from any other object in the system. In this sense, “created” state might be characterized as “ready, but inactive and isolated” state.

onStart():

At some point, due to user interaction, Android framework will call Activity’s onStart() method to let it know that it’s active now. This method is the right place to bring initialized, but inactive and isolated “created” Activity to life.

What exactly “bring to life” means depends on the requirements of each specific Activity. The general characteristics of the logic that resides in onStart() method are:

  1. Registration of View click listeners
  2. Subscription to observables (general observables, not necessarily Rx)
  3. Reflect the current state into UI (UI update)
  4. Functional flows
  5. Initialization of asynchronous functional flows
  6. Resources allocations

You might be surprised by the points one and two because in most official documentation and tutorials both of these are done in onCreate(Bundle). I see this as yet another misunderstanding of the intricacies of Activity life-cycle. Will get back to this point in discussion of onStop() method later in this post.

So, the general structure of onStart() method will be something along these lines:

    @Override
    public void onStart() {
        super.onStart();

        mSomeView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                handleOnSomeViewClick();
            }
        });

        mFirstDependency.registerListener(this);

        switch (mSecondDependency.getState()) {
            case SecondDependency.State.STATE_1:
                updateUiAccordingToState1();
                break;
            case SecondDependency.State.STATE_2:
                updateUiAccordingToState2();
                break;
            case SecondDependency.State.STATE_3:
                updateUiAccordingToState3();
                break;
        }

        if (mWelcomeDialogHasAlreadyBeenShown) {
            mFirstDependency.intiateAsyncFunctionalFlowAndNotify();
        } else {
            showWelcomeDialog();
            mWelcomeDialogHasAlreadyBeenShown = true;
        }
    }

Let me repeat again that what you actually do in onStart() will be determined by the detailed requirements of each specific Activity.

After onStart() method is called by the framework the Activity will transition from “created” into “started” state. In this state it is functional and can collaborate with other components of the system.

onResume():

The first rule about onResume() method is that you don’t need onResume() method. The second rule is the same. The third rule says that you might indeed need onResume() method in special circumstances.

Seriously though, I just searched through one of the codebases I’m working on. Found 32 overrides of onStart() averaging to about five lines of code each. This gives me about 150 lines of code in all onStart() methods combined.

In contrast, I found just two overrides of onResume() method that contain eight lines of code combined. In both cases these methods were overridden to resume on-screen animations and videos.

This summarizes my view of onResume() – it should only be used to start or resume some moving stuff on the screen. The reason why you’d want to do this in onResume() instead of onStart() will be discussed later in onPause() section.

After this method returns Activity transitions from “started” state into “resumed” state. This means that the user is interacting with it right now.

onPause():

In this method you should pause or stop on-screen animations and videos that you resumed or started in onResume(). Just like with onResume(), you hardly ever need to override onPause().

The reason why you handle animations in onPause() instead of onStop() is because when Activity is partially hidden by e.g. system dialog or looses focus in multi-window mode, only onPause() will be called. Therefore, if you want to play the animation only when the user actually interact with this specific Activity, avoid distracting him in multi-window mode and prolong battery life – onPause() will be the only reliable option.

The corollary to this is that if you want your animation or video to continue playing when the user enters multi-window mode, then you shouldn’t pause it in onPause(). In this case, move the logic from onResume()/onPause() into onStart()/onStop().

One special case which was brought to my attention is camera. Since camera is a single resource shared among all applications, usually you will want to release it in onPause() method.

However, that will make it impossible to use the camera in multi-window mode if the user interacts with another window. If that’s the desired functionality (e.g. video chat application), then you will need to release the camera resource in onStop().

After this method is called Activity will transition from “resumed” state back into “started” state.

onStop():

In this method you will unregister all observers and listeners and release all resources that were allocated in onStart().

I usually end up with something along these lines:

    @Override
    public void onStop() {
        super.onStop();

        mSomeView.setOnClickListener(null);

        mFirstDependency.unregisterListener(this);
    }

Let’s discuss why you actually want to unregister in this method.

First of all, if this Activity would never be unregistered from mFirstDependency then you would risk leaking it. This is not the kind of risk I will ever be willing to take.

So, now the question becomes why unregister in onStop() and not in, say, onPause() or onDestroy()?

It used to be quite tricky to explain why onPause() is not a good candidate, but addition of multi-window mode allowed for a quick and clear explanation.

When the Activity is visible in multi-window mode the users will expect it to be functional, even if they don’t interact with this Activity at the moment. Otherwise, why would they bother entering multi-window and keeping that Activity on screen?

Now, if Activity unregistered from mFirstDependency in onPause(), then it will not receive notification about asynchronous flow completion. Consequently, it will not be able to let the users know about this event, thus completely missing the point of multi-window. Not good.

Unregister in onDestroy() is also not a good option.

See, when the application is backgrounded by the user (e.g. click on “home” button), Activity’s onStop() will be called thus returning it to “created” state. Activity can remain in this state for days and even weeks.

If mFirstDependency produces a continuous stream of events, this Activity can handle this stream for weeks, even if the user never actually interacted with it during this period. That would be an irresponsible waste of user’s battery life.

Similarly, being registered to mFirstDependency could be associated with high memory consumption. In this case, the entire application would become more appealing to Android’s low memory killer while in background.

So, neither onPause() nor onDestroy() are places to unregister from external dependencies, therefore you should do it in onStop().

A word about unregistering from View objects.

Due to unfortunate design of Android UI framework, weird scenarios like the one described in this SO question can happen. There are several ways to work around that, but unregistering from UI elements in onStop() is the cleanest one IMHO.

Alternatively, you can ignore this rare scenario altogether. That’s what most Android developers do, but, then, your users will occasionally see weird behavior and crashes.

Unfortunately, making this kind of trade-offs is everyday routine for professional Android developers.

After onStop() method completes Activity transitions from “started” state back to “created” state.

onDestroy():

This method should never be overridden. Literally, never.

I just searched in all my projects – not a single place where I needed to override onDestroy().

This makes total sense if you consider what I said about the responsibilities of onCreate(Bundle). Since it only initializes the Activity object without doing any work, there is nothing that needs to be done manually to clean things up.

When I review new Android codebases, I usually search for several common mistakes and anti-patterns. This gives me a quick indication of the quality of the code. Overrides of onDestroy() are among the very first code smells that I look for.

onSaveInstanceState(Bundle):

The last piece of mosaic called Activity’s life-cycle that I often use is onSaveInstanceState(Bundle) method.

This method is used to preserve data over configuration change and save & restore flows. If you aren’t familiar with these concepts, you should definitely read the article about Android Memory Management and Save & Restore.

The only operations you should do in this method is pushing the state you’d like to preserve into the provided Bundle data structure:

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean(SAVED_STATE_WELCOME_DIALOG_SHOWN, mWelcomeDialogHasAlreadyBeenShown);
    }

In this context I would like to mention one very common pitfall related to Fragments.

If you commit Fragment transactions after this method completed, the application will crash with the notorious IllegalStateException. This is an entire world of ugliness on and by itself and I wouldn’t want to go into its depths right now. If you don’t know what I’m talking about, then you can read this post by Alex Lockwood (though it might be a bit outdated today).

What is important to know is that onSaveInstanceState(Bundle) will be called before onStop().

This means that the Activity remains subscribed to external objects and internal Views after its state is saved. Therefore, if Fragment transaction will be attempted due to invocation of a callback or user interaction in between onSaveInstanceState(Bundle) and onStop() – crash will occur.

There are several approaches to handling these edge cases:

  1. Do nothing and accept that there might be some crashes
  2. Commit Fragment transactions using the method commitAllowingStateLoss() and accept that some state can be lost
  3. Don’t use Fragments at all
  4. Handle each case individually, risking missing some edge scenarios and still getting the crashes

Have I already mentioned that making hard trade-offs is everyday routine of professional Android developers?

The good news are that after years of struggle Google finally acknowledged that this case was basically a bug. The ordering of method calls in Android P will be changed such that onStop() will be called before onSaveInstanceState(Bundle).

This means that if you follow the scheme I described in this post, starting with Android P you will not see IllegalStateException due to Fragment transactions anymore, even if you chose the first approach from the above list.

Edit 1:

Redditor Boza_s6 provided an invaluable feedback on this article:

But even with current behavior I don’t think there’s a problem if everything is cancelled in onStop, because AFAIK onSaveInstanceState and onStop are called as part of same message, so there’s no chance for something to be executed between them.Boza_s6

I have never read or heard about this, but it sounded interesting and important enough to actually give it a thorough investigation. So I opened my copy of AOSP and after some grepping I ended up looking at ActivityThread#performStopActivityInner() method.

Both onSaveInstanceState(Bundle) and onStop() originate from this method. I traced the respective execution paths to make sure that there are no more Runnables posting involved and there weren’t.

Then I dig into AOSP Git history to see since when these methods had been called together.

I’m happy to tell you that this implementation choice was made back in the Honeycomb days. This means that if you use the approach suggested in this post, you’re already safe from crashes due to Fragment transaction commits after state save.

I wouldn’t recommend you write applications based on undocumented implementation detail, but since the ordering of methods will be changed in Android P – you risk absolutely nothing.

So, you can forget about Fragment transactions after state save. You are already safe. Just make sure that you follow the approach I described and your Activities become “inactive and isolated” after onStop().

Edit 2:

Turns out “Edit 1” above is not entirely correct.

There is yet another corner case which was brought up by redditor RaisedByTheInternet. [BTW, have I already mentioned that androiddev subreddit is one of the best resources for professional Android developers to follow?]

As stated on the official Handling Lifecycles page:

On API level 23 and lower, the Android system actually saves the state of an activity even if it is partially covered by another activity. In other words, the Android system calls onSaveInstanceState() but it doesn’t necessarily call onStop(). This creates a potentially long interval where the observer still thinks that the lifecycle is active even though its UI state can’t be modified.

So, there is no “interval” between onSaveInstanceState(Bundle) and onStop() only if the Activity is actually stopped. However, on API 23 and lower, onSaveInstanceState(Bundle) can be called without onStop() if the Activity is being partially obscured by another Activity.

What are the implications of all this?

First of all, “Edit 1” above is incorrect for API 23 and lower because state loss can still happen in partially obscured Activities. This is a very important point and I’m very grateful to redditor RaisedByTheInternet for correcting me.

Secondly, you will still need to choose your strategy from the four options I listed above (before the two edits).

Given the fact that this “partially obscured by other Activity” scenario is rare and this entire issue is not relevant for API 24 and above, options 1 and 2 become much more appealing to me personally.

Conclusion:

Alright, time to conclude this article.

It ended up being neither short nor simple, but I think it is still simpler and more complete than most of the resources out there (including the official documentation, unfortunately).

I know that some experienced professional developers will challenge my way of handling Activity life-cycle and that’s alright. In fact, I’m eagerly waiting for it.

However, keep in mind that I’ve been developing and using this scheme for several years now. In my experience, the code written using this approach ends up much cleaner than the horrors and mess of life-cycle handling logic that you see in many projects.

The final validation of this approach came from Google itself.

When multi-screen support landed in Nougat, the approach I shared with you in this article required almost zero adjustment. This basically confirmed that it incorporated a much deeper insights about Activity life-cycle than the officially recommended ones.

In addition, the change in ordering between onStop() and onSaveInstanceState(Bundle) method calls in Activities in Android P will make this approach the safest for working with Fragments.

I don’t think that this is any kind of coincidence.

In the next post I’ll explain how to handle Fragment’s lifecycle in a similar way.

As always, leave your comments and questions below.

Check out my top-rated Android courses on Udemy

Subscribe for new posts!

10 Comments

  1. vladchuk April 11, 2018 at 6:02 pm - Reply

    Do you have any plans to evaluate and write about Flutter?

    • Vasiliy April 11, 2018 at 6:16 pm - Reply

      Hello Vladchuk,
      I will speculate a bit about the business motivation behind Flutter in one of my upcoming posts, but I don’t intend to try it any time soon.

      • Tyler Getsay April 11, 2018 at 7:33 pm - Reply

        Do you have any tool you like? Or one that seems interesting?

        • Vasiliy April 11, 2018 at 7:59 pm - Reply

          Hi Tyler,
          There are many tools I use when developing for Android. Most of them are about productivity, others about quality assurance.

          External tools from the top of my head:
          Emacs – I read all text and code outside of the project I’m working on with it; especially powerful for quick deep dives into AOSP
          Cygwin – linux shell environment emulator for Windows
          Command line Git – I find it quicker and easier to work with Git in command line
          p4merge – my favorite GUI for reviewing Git diffs and resolving merge conflicts
          Fiddler – arguably the best web debugging proxy (too bad it is not available on Macs)

          Libraries and frameworks:
          EventBus – allows for an easy decoupling of components in the app (but can be easily abused)
          GitHub Immutables – generator of immutable data structures
          Stetho – integrated debug bridge; I mostly use it to capture HTTP traffic when testing on real devices

          However, overall, my best tools are design, architecture and unit testing.

  2. Ahmad April 12, 2018 at 12:21 am - Reply

    I’ve never seen anyone setting onClickListener to null before so I’ve never really thought about it, but I guess it makes sense. Would you say it’s actually necessary?

    • Vasiliy April 12, 2018 at 7:15 am - Reply

      Hi Ahmad,

      I would say that it is not mandatory because the vast majority of applications don’t do it and work alright. I mean they surely get some amount of errors and crashes due to this unfortunate corner case, but, in general, I don’t think it’s a deal breaker.

      That said, if you can avoid this case with minimal investment then it might be a good idea to do so.

      For example, my approach to presentation layer architecture in Android (summarized in this series of articles) allows for “detaching” of all UI elements with a single line of code. I didn’t know about this corner case when I came up with this approach, so when I discovered it, the fact that I can easily handle it provided a huge validation to my architectural decision.

  3. Dmitriy Morozov April 12, 2018 at 4:10 pm - Reply

    Could you write such a confident article about Fragments lifecycle? Thank you very much for a complete explanation. Much appreciated!

    • Vasiliy April 12, 2018 at 4:29 pm - Reply

      Hi Dmitriy,
      Thank you for the feedback. Knowing that developers find these long posts useful is very important for me (and, surely, flattering).
      I do intend to write a similar article about Fragments. However, this will take some time.
      See, I’ve been promising my readers an in depth review of Google’s adoption of Kotlin for months now. So, the next three articles will be dedicated to this (business) topic and then, hopefully, I will get back to engineering and write about Fragments.
      Don’t forget to subscribe for post notifications if you’re interested in any of these 😉

      • Dmitriy Morozov April 12, 2018 at 4:35 pm - Reply

        Yeah, that’s a great amount of text I think. The main point is that the post fairly exhausted the topic. I found it rather helpful to complete my view on the lifecycle.

      • Mike May 22, 2018 at 7:05 pm - Reply

        I also find your articles very useful. I first encountered with your articles about MVP, when I tried to understand the pattern as a newbie developer. Then your theoriy about Flutter and Kotlin and Google killing Android is shared to me. That’s when I subscribed to you. Now I’m here because of your new article about fragments. I really like to understand what am I doing and I want to be a good developer. So far you explained the topics very comprehensible and I was able to understand the logic behind the coding. And this is what most important to me! If I understand the essence of developing, I can reproduce good quality codes anytime I want.

Leave A Comment

Stay Connected!

Receive notifications about new posts and resources straight into your inbox!
SUBSCRIBE
close-link