Don’t Keep Activities

Matt Robertson
ProAndroidDev
Published in
4 min readAug 24, 2021

--

“Here’s a screwdriver. You can use it to poke a hole in your gas tank, so you can see what happens if it ever springs a leak.”

At work recently I was working to fix a common crash we were seeing in our analytics, coming from the splash screen. It was a bizarre bug to begin with, but it was also the worst kind of bug: the dreaded “can’t reproduce” bug that you know is an actual bug because so many of your users are hitting it.

Looking at the crash stack trace, it seemed like the crash was resulting from accessing a ViewModel with fragment-ktx’s activityViewModels() after the Activity had gone out of scope. My hypothesis was that the crash was from users who were closing the app while the splash screen was still being displayed. (For context, we use a single-activity framework with the Jetpack Navigation library, and we display the branding splash screen fragment for 1 second before navigating to the next fragment). And I had a good idea as to what a possible fix might be.

But here’s the million-dollar question: how do I know when I’ve actually fixed the bug if I can’t reproduce the crash? I can apply my fix, run the app, and see no crash — but that was already the case on my end, so that doesn’t give any confidence that my fix was a correct one. Aside from taking a guess, merging to master, and pushing a new experimental build to our beta testers like they were guinea pigs… how could we know that our fix had actually solved the problem that our users were experiencing? We needed to be able to reproduce the crash.

I tried launching the app and immediately closing it, but my device wasn’t killing the Activity in the way that some users, most likely with lower-end devices, seemed to be experiencing. I tried creating low-end emulators, but I still couldn’t get the Activity to go out of scope. And even if I could, if I couldn’t do it 100% of the time then I wouldn’t have confidence that my hypothesized fix had worked. (Ideally I would be writing an Espresso test for this, but our project hasn’t used UI tests in the past and unfortunately hasn’t quite reached the refactor stage where this would be a feasible approach within our dev cycle).

So what could we do in this real-world situation with a real-world bug in a real-world app with real-world users and real-world testing constraints?

“Don’t Keep Activities” to the rescue

There is an interesting setting buried in the Android “Developer options” called “Don’t Keep Activities.” This setting destroys every Activity as soon as you leave it. At first glance it seems irrelevant. I was suspicious of it at first. If my users aren’t running their devices with “Don’t keep activities” enabled then why should I test with “Don’t keep activities” enabled? Won’t I be finding bugs that don’t actually exist in the real world?

But for situations like my splash screen crash, this setting can be invaluable. Let’s return to the problem: How can I reproduce a crash that only occurs on some low-end devices where our app’s Activity is destroyed within one second of backgrounding the app? With “Don’t Keep Activities” enabled, this becomes 100% reproducible, since the Activity is always destroyed as soon as the user leaves the app. After trying without success to reproduce this crash on my testing devices, personal devices, and multiple emulators, I enabled “Don’t keep activities” on my Pixel 4, went through my hypothesized scenario of closing the app during the splash screen, and immediately was rewarded with the exact stack trace I’d been seeing in our analytics.

It’s a bit of a brute-force solution, no doubt. But in this particular case it was the one that worked. My manager (who originally mentioned the setting to me) described the setting well: “Here’s a screwdriver. You can use it to poke a hole in your gas tank, so you can see what happens if it ever springs a leak.” It’s not elegant, to be sure. But it’s handy to have in your toolkit.

Give it a spin. Enable “Don’t keep activities” on your device and play around with an app you’re working on. You might just be horrified to learn how some of your users with lower-end devices experience your app on a daily basis.

p.s. For those who are curious, we found it extremely helpful to use lifecycleScope.launch { delay(1000) } instead of postDelayed({}, 1000). This ensures that after 1000ms the operation will only run if the lifecycle is still in scope. Once again, coroutines to the rescue.

Follow for more on best practices in Kotlin and Android development.

--

--

Android Engineer @ Crossway. Writing on clean arch, clean code, Kotlin, coroutines, & Compose.