Ways to test Android killing your stuff
A while ago, I submitted the issue “No way to test ‘System needs resources’ situation using ActivityScenario” in which I detailed a few ways to simulate various Activity recreation scenarios in Robolectric (using ActivityController
) that weren’t possible using AndroidX’s ActivityScenario
. For my own benefit as much as anyone else’s, I wanted to go a bit deeper into exploring the different scenarios in which an Activity is recreated in Android, and how to simulate those using Robolectric so that you can test your Activity’s behaviour during them. I’m not going to be concerning myself with scenarios where the app is restarted fresh like from a “Force Stop” or a power cycle.
It’s worth noting that you can hopefully avoid needing to care about these scenarios by doing good things like avoiding static/application state, adhering to the tenants of unidirectional data flow and never making any mistakes. If you’re a mere human however, understanding these scenarios and protecting against bugs that they might cause is probably important. It’s also worth noting that, with some noted exceptions, I haven’t found a lot of solid documentation on any of this, so feel free to shout at/correct me about anything you read here that is incorrect.
As far as I’m aware, there are 3 distinct Activity recreation scenarios:
In this post, I’ll attempt to document these scenarios (along with a nasty twist on them) and give examples of how they can be simulated using Robolectric.
As far as I’ve seen, this is the only Activity recreation scenario that’s been well documented and can be easily tested using AndroidX Test with instrumentation or local tests. The basic premise here is that when something Android thinks of as “configuration” (like screen size, orientation, dark/light mode etc) changes at the system level, Activity object will be recreated with that new configuration. Before this happens, the system calls Activity#onSaveInstanceState
which creates a Bundle
that will eventually be passed to Activity#onCreate
for the new instance of the Activity. That “saved instances state” Bundle
can therefore be used to persist state between configurations. On top of that there’s two pretty big things to keep in mind:
ViewModelProvider
) making them a good place to keep state that should stick around between rotations etc.DialogFragment
instances) will be recreated as part of your super.onCreate
call. This is useful for doing things like keeping dialogs on screen between configuration changes, but it can cause problems if your code doesn’t take into account setting things up for these recreated Fragments. A prime example of this that you’ll need to have any custom FragmentFactory
setup taken care of before super.onCreate
so that Fragments can be recreated.Here’s how to simulate this scenario using Robolectric:
val activityController = Robolectric.buildActivity(MyActivity::class.java)
.setup()
activityController.recreate()
As I said before, you can also use AndroidX Test for this:
val activityScenario = ActivityScenario.launch(MyActivity::class.java)
activityScenario.recreate()
This can happen if the system wants to reclaim resources (most likely memory) used by an Activity in the background that’s been paused/stopped. My understanding (or guess) is that this would only happen if Android wants to reclaim resources from a current “foreground” (on screen) app as it can just destroy the whole process (and then restart them as we’ll explore later) for apps in the background. It’s easy enough to force this behaviour whenever an Activity is paused by enabling the “Don’t keep Activities” setting in Developer settings.
Like with configuration changes, onSaveInstanceState
can be used to retain state throughout this recreation and Fragments are recreated. A big difference however is that ViewModel instances do not survive. I’d imagine this is to allow whatever memory they consume to be reclaimed. To avoid clumsily passing state between your Activity and ViewModels so that it can be retained as part of the onSaveInstanceState
Bundle
, Jetpack provides a Saved State module that hides all that from you.
Because the ActivityContoller
and ActivityScenario
recreate
methods are implemented in such a way that ViewModels are retained (like during configuration changes), we can’t use it to simulate this scenario. Fortunately though, ActivityController
does give us enough control of the lifecycle to do it ourselves:
val initial = Robolectric.buildActivity(MyActivity::class.java)
.setup()
val outState = Bundle()
initial.saveInstanceState(outState)
.pause()
.stop()
.destroy()
val recreated = Robolectric.buildActivity(MyActivity::class.java)
.setup(outState)
The subtle difference from our configuration changes example is that we’re manually destroying the Activity while retrieving it’s saved instance state and then creating a new ActivityController
with that saved instance state. This will give us a new Activity that uses the old Activity’s saved instance state, but will not have access to the old ViewModels. As you might have guessed, an Activity’s FragmentManager
state is persisted as part of the saved instance state bundle, so we also get recreated Fragments here.
It isn’t possible to write a test like this using ActivityScenario
: we can tear down the Activity under test with ActivityScenario#moveToState
and even use ActivityScenario#onActivity
to cheekily poke Activity#onSaveInstanceState
, but we have no way of passing that instance state to a new instance of the Activity (a large part of the earlier mentioned issue).
As mentioned earlier, Android will occasionally destroy app processes in the background to reclaim memory. This probably happens more than you realize according to dontkillmyapp.com. When this happens, the app’s process and current back stack is destroyed (with corresponding Activity#onSaveInstanceState
calls) and then both are recreated when the user navigates back to the app. Because the back stack is recreated, we do again get to keep our saved instance state Bundle
and our Fragments, but we’ll lose ViewModels and any “process” level state (Java static
or state we’ve attached to the Android Application
). You can force this behaviour to happen whenever you switch between apps (the one now in the background will be destroyed) using the “Background process limit” setting in Developer settings.
I’m unable to provide a one-size-fits-all solution to simulating this scenario as what state needs to be reset or initializers that need to be run to simulate the process restart will be different for every app. Here’s an example that you can bring your own resetProcess
implementation along to however:
val initial = Robolectric.buildActivity(MyActivity::class.java)
.setup()
val outState = Bundle()
initial.saveInstanceState(outState)
.pause()
.stop()
.destroy()
resetProcess()
val recreated = Robolectric.buildActivity(MyActivity::class.java)
.setup(outState)
As you might have spotted, this is identical to our system recreates Activity example with the addition of resetProcess
between our Activity destruction and creation. It’s also probably obvious but still worth pointing out that implementing a realistic version of resetProcess
is always going to be challenging as you might not be aware of every piece static state in your app. I’d definitely suggest putting your app through this scenario manually in an emulator or a test device to discover any problematic state you might have.
You forgot about Activity#startActivityForResult
right? Although it shouldn’t cause you any problems during configuration changes, it can add some real headaches to the system recreates Activity and system recreates process scenarios. Imagine that MyActivity
from our examples starts another Activity ResultActivity
for result. Android could destroy MyActivity
to reclaim resources while ResultActivity
is visible, or it might even destroy the whole process if the user navigates away. When ResultActivity
returns the result after either of these scenarios, onActivityResult
will be called after MyActivity#onCreate
is called during recreation which might get you in trouble if you’re loading state you expected to have ready already or if you have something important in Activity#onResume
. Again, we can fortunately simulate this with some (arguably far nastier) Robolectric:
val initial = Robolectric.buildActivity(MyActivity::class.java)
.setup()
// Action to start `ResultActivity` for result
val outState = Bundle()
initial.saveInstanceState(outState)
.pause()
.stop()
.destroy()
val recreated = Robolectric.buildActivity(MyActivity::class.java, this.intent)
.create(outState)
.start()
.restoreInstanceState(outState)
.postCreate(outState)
val startedActivityForResult = shadowOf(initial.get())
.nextStartedActivityForResult
shadowOf(recreated.get()).receiveResult(
startedActivityForResult.intent,
resultCode,
result
)
recreated.resume()
.visible()
.topActivityResumed(true)
Here we have to manually execute the lifecycle steps that ActivityController#setup
was handling for us so that we can simulate the result being received (via ShadowActivity#receiveResult
) between postCreate
and onResume
. Like with the examples before, we can simulate the system recreates process version of this by inserting our resetProcess
call before recreating the Activity.
As far as I’m aware, the lifecycle steps and their ordering would be the same here if you opted to use the new Activity Results API instead of using the now deprecated startActivityForResult
/onActivityResult
directly (which you should if you can).
We’re at a point where we need a fairly noisy amount of code to simulate these scenarios, and in practice these tests would be very hard to read. I’ve wrapped all this up in an extension for ActivityController
to make my (and hopefully your) life a little easier:
inline fun <reified A : Activity> ActivityController<A>.recreateWithProcessRestore(
resultCode: Int? = null,
result: Intent? = null,
noinline resetProcess: (() -> Unit)? = null
): ActivityController<A> {
// Destroy activity with saved instance state
val outState = Bundle()
this.saveInstanceState(outState).pause().stop().destroy()
// Reset process if needed
if (resetProcess != null) {
resetProcess()
}
// Recreate with saved instance state
val recreated = Robolectric.buildActivity(A::class.java, this.intent)
.create(outState)
.start()
.restoreInstanceState(outState)
.postCreate(outState)
// Return result
if (resultCode != null) {
val startedActivityForResult = shadowOf(this.get())
.nextStartedActivityForResult
shadowOf(recreated.get()).receiveResult(
startedActivityForResult.intent,
resultCode,
result
)
}
// Resume activity
return recreated.resume()
.visible()
.topActivityResumed(true)
}
There was probably a way to do this without reified
, but what fun would that be?