Hey Android, Please Keep My Activities!

Calvin Noronha
MindOrks
Published in
8 min readOct 3, 2019

This post is about insights I acquired, while trying to debug a crash due to an Activity destroy in the background. I’ll explain why testing rotation changes doesn’t guarantee that your application is handling State Saving properly, and also why it’s so critical to test your applications using the “Don’t Keep Activities” option in Developer Settings.

I’ve implemented a project that demonstrates a crash, and then follows through to fix the bug. We’ll closely follow what a developer would do in a Development → Testing → Release cycle.

I highly recommend you go through the entire article. If you don’t have the time to go through the app section: the Prerequisites before the app, and the Learnings / TL;DR at the end should be enough to get you up to speed. Since this is gonna be a long one, I’ve broken it down into sections:

  • Introduction
  • Prerequisite Knowledge for State Saving on Android
  • The Game Implementation
  • Testing The App, Comparison with Ideal Behavior
  • Finding the bug, Understanding the Cause, Fixing it
  • Learnings / TLDR;
  • Bonus: Kotlin @Parcelize

Let’s get started, shall we?

Intro: Wouldn’t it be magical if our devices had unlimited memory?

Yeah, it definitely would. Android would keep every object you create in memory. We wouldn’t have to deal with saving and retrieving state, and wouldn’t have to worry about debugging memory leaks, since we have unlimited memory to work with!

Unfortunately that isn’t physically possible, and the apps we write are given 50–100 MB heap memory at best, 12 MB at worst, according to this Stackoverflow post. Most devices in the market right now have ~3GB of RAM.
With multiple apps running in the background, and the foreground applications being the most prioritized, Android freely kills off Activities and even entire processes to serve more RAM to apps that have the user’s attention. We can increase the heap space of our apps using the largeHeap attribute in our Manifest, however this would actually prepone your Activity kill if other apps are doing the same.

To give users the best experience, we should be able to save and restore the user’s state, with any text or inputs they’ve done, irrespective of whether our app was alive or killed in the background

For us developers, this means our apps should handle being killed off to save memory. And when restored to the foreground, our app should behave as if Android never really killed the app.

Prerequisite Knowledge and Best Practices for Saving UI States

  • To save your View State, put your member variables into the Bundle in onSaveInstanceState: This is the official and recommended approach to save your Activity state. You put your variables and objects into the Bundle, and you receive it again in your new Activity’s onCreate() to restore your UI. If you didn’t know this, read the official documentation.
  • Android recreates Activites on rotations: Yep, take my word for it. When you rotate your Android Device, you will see a call to onPause() → onStop() → onSaveInstanceState() → onDestroy(). Then, a new instance of your Activity will be created, and onCreate() will be called with a Non-Null Bundle savedInstanceState.
  • When passing state to Fragments, always use Arguments: Fragment Arguments, unlike member variables, are automatically saved and restored for you. All you need to do is retrieve your state from these arguments, whether created or re-created. These are put into a Bundle and set on the Fragment via setArguments(Bundle)
  • Moving an app to the background and foreground causes an onStop, onStart cycle: Again, if you’re unfamiliar with the Activity Lifecycle in the background, read the official documentation.

Let’s Play A Game!

Screens of the ‘Dont Keep Activities’ app

I’ve built an app to simulate the bug and fix it in another branch. You can find the source code and follow along on GitHub.

The game consists of 2 simple screens:

  • The PlayerDetailsFragment asks for Player Names. When “Play” is clicked, we retrieve the Strings from the EditTexts and push them via arguments to the GameFragment.
  • The GameFragment creates a GameState from these PlayerDetails, which consist of the scores of the Players. When the GameFragment gets created, it reads the Player Details from Arguments, and shows it on the UI.
  • Clicking on the “+” buttons besides the Player Names increments the score of the Player.

Finally, here’s the implementation of GameState:

App Testing, Debugging & Findings:

Now that we’ve implemented the app, let’s test it. You can follow along with the APK here.

Scenario #1: Enter player names and rotate the device
Expectation: The Player Names are populated on screen after rotation
Actual: Works fine, since Android itself saves and restores the state of any Views that have an ID in XML.

Scenario #2: Enter player names, click Play, rotate the device
Expectation: The Game Screen should show, rather than asking for the Player Names again.
Actual: Works fine, we’ve handled this explicitly in MainActivity’s onCreate

if (savedInstanceState == null) inflatePlayerDetailsFragment()

Scenario #3: Enter player names, click Play, increment player scores, rotate the device
Expectation: The Game Screen should show, with the Player’s scores intact
Actual: Works fine. The GameFragment saves and restores our GameState via Arguments

Scenario #4: On any screen, background the app and then foreground it
Expectation: The same screen should appear, with state intact
Actual: Works well!

Alright, the app looks good. We’ve done our testing, now it’s time to ship!

On trying the above scenarios with “Don’t Keep Activities” on, it’ll crash on scenario #4 when on the GameFragment 100% of the time. But why?

I encourage you to try and debug the crash yourself. However, if you prefer to follow along, here’s the shortened stacktrace of the crash:

2019-09-28 14:47:12.511 5087-5087/com.calvinnor.dontkeepactivitiesgame E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.calvinnor.dontkeepactivitiesgame, PID: 5087
android.os.BadParcelableException: ClassNotFoundException when unmarshalling:
at android.os.Parcel.readParcelableCreator(Parcel.java:2831)
at android.os.Parcel.readParcelable(Parcel.java:2757)
at com.calvinnor.dontkeepactivitiesgame.data.GameState.<init>(GameState.kt:15)
at com.calvinnor.dontkeepactivitiesgame.data.GameState$CREATOR.createFromParcel(GameState.kt:32)
at com.calvinnor.dontkeepactivitiesgame.data.GameState$CREATOR.createFromParcel(GameState.kt:30)
at android.os.Parcel.readParcelable(Parcel.java:2766)
at android.os.Parcel.readValue(Parcel.java:2660)
at android.os.Parcel.readArrayMapInternal(Parcel.java:3029)
at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:288)
at android.os.BaseBundle.unparcel(BaseBundle.java:232)
at android.os.Bundle.getParcelable(Bundle.java:940)
at com.calvinnor.dontkeepactivitiesgame.ui.GameFragment$gameState$2.invoke(GameFragment.kt:16)
at com.calvinnor.dontkeepactivitiesgame.ui.GameFragment$gameState$2.invoke(GameFragment.kt:13)
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
at com.calvinnor.dontkeepactivitiesgame.ui.GameFragment.getGameState(Unknown Source:7)
at com.calvinnor.dontkeepactivitiesgame.ui.GameFragment.setupPlayerNames(GameFragment.kt:80)
at com.calvinnor.dontkeepactivitiesgame.ui.GameFragment.onViewCreated(GameFragment.kt:64)

On closer inspection, we find that the crash is due to not being able to create a GameState from the Parcel. And if we look at the GameState, our bug is evident.

When writing GameState into the Parcel, we forgot to write the Player Two’s details 🤦

override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(playerOneDetails, flags)
parcel.writeLong(playerOneScore)
parcel.writeLong(playerTwoScore)
}

It’s this silly mistake that crashes the app. And an easy fix to ship the updated version. However, this brings us to the insight of this long blog post:

The app didn’t crash on rotations, or background-foreground. It only crashed when “Don’t Keep Activities” was turned on. This means Android does not Serialize / Parcelize your data classes otherwise, and the code to accomplish this process is untested. Which is an obvious optimisation, why would Android spend time to serialize / parcelize your Bundle when it has enough RAM to hold the object in memory? Including rotations, when you already know that your Activity is going to be recreated, and you’ll need this object with the same memory requirements.

Learnings, TLDR;

With reference to our prerequisites & best practices:

  • Android recreates Activities on rotation. However, when rotating the device, the same Bundle is cached in memory and passed back to you. This means any of the objects or nested objects you put into the Bundle during onSaveInstanceState() isn’t actually serialized / parcelized. You simply put it into the Bundle, Android saved it in-memory, recreated the Activity for you and passed the same Bundle instance.
    But how do you know? Add a debugger to onCreate() and onSaveInstanceState(), and see the Java Object Reference of the savedInstanceState Bundle. When rotating, you’ll receive the same instance, whereas when your Activity dies, persists the Bundle contents, and restarts, the Android system creates a new Bundle from the persisted data.
  • Moving an app to the background and foreground causes an onStop, onStart cycle. However, onDestroy is never called unless your Activity dies in the background. This is why it’s so critical to use the Don’t Keep Activites option. It simulates a low-memory device putting your app in the background which kills the Activity, and then returning to it with the savedInstanceState. This can also happen on high RAM devices, where the user wants to run a game that needs more memory.
  • Bundle contents are not serialized / parcelized to memory unless required. This includes Fragment Arguments, passing data in Intents, etc. Which means, as of now, there is no way to test your Parcelization code apart from Android killing your Activity in the background.

Put simply:

After you’ve built your app, it’s unethical to rotate your device to see if everything’s working fine, and then ship it. You never really tested your app on a low-memory device, or what happens when it’s backgrounded and killed due to memory constraints.

Testing with Don’t Keep Activities is a fundamental part of the Android app development lifecycle, and shouldn’t be ignored. It can be a make or break situation with your users when your app properly restores UI state when coming to foreground, vs your competitor’s app who just doesn’t care for it.

Bonus: Kotlin’s @Parcelize

The primary issue with the bug we fixed was a user error. If we’re implementing Parcelable in our classes, we need to remember to update the writeToParcel() and createFromParcel() methods whenever we add or remove a class variable.

Fortunately, Kotlin gives us a nice @Parcelize annotation that we can apply to our classes, to auto-generate the Parcelable implementation. Here’s an implementation of the PlayerDetails class with @Parcelize

Neat, huh? You just need to add this to your build.gradle’s android block and you’re good to go!

androidExtensions {
experimental = true
}

Note that it’s still experimental, so I advise you to test the Parcelization with Don’t Keep Activities, instead of blindly trusting the plugin.

GitHub Repository:

You can find the Don’t Keep Activities Game here.
A brief overview of the project along with branches are mentioned there.

Further Reading:

And That’s It! Go forth and write better apps :)

I’m Calvin Noronha, a Software Engineer who loves to write code. High-five this post if I helped you, or leave a comment. Any feedback is welcome!

You can also reach me on Twitter — @CalvinNrnha

--

--

Calvin Noronha
MindOrks

Software Engineer. Tech Enthusiast. Open Source contributor. UX Engineer @ Google