DevelopmentAll levels

Android / Kotlin

Android development means building apps in Kotlin for a system that owns each app’s lifetime and may rebuild a screen or kill the whole process at any time. That one fact explains the platform’s most common bug — a screen that comes back empty — and nearly every architecture recommendation Google makes. The page opens with sixteen events that destroy a screen and what survives each, then covers process death and saved state, Kotlin and coroutines up to Kotlin 2.4, Compose and its three phases, background work, Google Play’s tracks and its 2026 calendar, five documented cases, and where it degrades.

Current version
Kotlin 2.4 · Android 17 · Compose 1.12
Lifetime
Owned by the system — rotation rebuilds, memory pressure kills
Google Play
Target API 36 required since 31 Aug 2026

What destroys your screen — sixteen events, and what survives each

On Android the system, not your app, decides how long a screen and its process live. An activity — the object that runs one screen — is destroyed and built again whenever the device’s configuration changes. The whole process is killed whenever the system needs its memory, and when the user comes back, Android rebuilds the screen they left as if nothing had happened. Each event destroys a different amount, and a piece of state survives an event only if it lives somewhere that event does not reach. So the useful question about any screen is not “does it work?”. It is “what is left of it after each of these?”. Here are sixteen events. Later sections cite the rows by number.

#EventWhat Android doesWhat survivesHow to test it
1Pressing Home or switching to another appStops the activity; the process staysEverything — for nowPress Home, come back
2Navigating forward from a FragmentDestroys the fragment’s view; keeps the fragment on the back stackThe fragment and its ViewModel — not its view or anything bound to the viewNavigate there and back with LeakCanary installed
3Navigating away from a Compose destinationDisposes its composition; keeps its back-stack entryThe entry’s ViewModel and rememberSaveable — not rememberNavigate away and back
4The phone lies unused and unplugged (Doze)Defers network access, jobs and alarms to short maintenance windowsEverything — but background work waitsadb shell dumpsys deviceidle force-idle
5Rotating the deviceDestroys the activity and creates a new one; the process staysViewModel, saved state, disk — not the old activity object, not rememberRotate the emulator
6Switching dark modeAs row 5As row 5The Quick Settings tile
7Changing the app’s language (per-app language, since Android 13)As row 5As row 5Settings → Apps → Language
8Changing the font size or display sizeAs row 5As row 5Settings → Display
9Resizing the window — split screen, a foldable unfolding, a desktop windowAs row 5; on screens 600 dp and wider an app targeting API 36 can no longer lock its orientation, and from API 37 it cannot opt outAs row 5A resizable or foldable emulator
10Connecting a hardware keyboardAs row 5; from Android 17 the on-screen keyboard is not shown again unless the app asksAs row 5Attach a keyboard to the emulator
11The system needs memory while the app is in the backgroundKills the process; when the user returns, a new process rebuilds the screen from its saved stateSaved state and disk — not the ViewModeladb shell am kill <package> with the app in the background, then reopen it from Recents
12The user revokes a permission in SettingsKills the process; returning restores the screen as in row 11As row 11Revoke a permission while the app is in the background
13The back gesture on the last screenFinishes the activity; for apps targeting API 36, onBackPressed() is no longer calledDisk only — the saved state is discardedSwipe back
14Swiping the app away in RecentsRemoves the task and its saved state; the process usually ends tooDisk onlySwipe it away
15Force stop in SettingsKills the process; its alarms and jobs stop until the user opens the app againDisk onlySettings → Apps → Force stop
16Rebooting the phoneEnds everything; WorkManager reschedules its work, AlarmManager alarms are goneDisk, and WorkManager’s queueReboot the emulator
An Android activity is destroyed and rebuilt far more often than people expect — a rotation does it, and so does a language change, a window resize or a theme switch. Separately, the system can kill the whole process while the app is in the background and rebuild it when the user returns. The three tiers underneath are the practical answer: a ViewModel survives recreation but dies with the process, a saved state handle survives both but holds only small values, and only real storage survives everything.

Process death, saved state and memory

Rows 11 and 12 exist because a phone runs dozens of apps in a few gigabytes of memory and does not page app memory out to disk; it compresses it instead. When that is not enough, a system service, the low-memory killer, ends processes in order of importance: cached apps in the background first, then less important ones. On a mid-range phone with a camera or a game in the foreground, your app in the background can be gone within seconds. Android then keeps a promise the user never sees: when they come back from Recents, a new process is started and the screen they left is rebuilt — same tab, same scroll position, same half-typed message — if the app saved them.

The mechanism is saved state. As an activity stops, Android asks it for a Bundle — a small map of values — and keeps it outside the app’s process, so it outlives the process and is handed to the new one. SavedStateHandle in a ViewModel and rememberSaveable in Compose are both written there. The Bundle travels through Binder, Android’s mechanism for calls between processes, whose buffer is 1 MB and shared by everything the process is sending at that moment. Google recommends keeping saved state under 50 KB, and since Android 7.0 going over the limit throws TransactionTooLargeException, a crash that happens on the way into the background.

Where the state livesRows 5–10: rebuiltRows 11–12: process killedRows 13–16: session endedUse it for
remember in a composableLostLostLostValues cheap to recompute — an animation, a filtered list
A ViewModelKeptLostLostWhat can be fetched again; work in flight
Saved state — SavedStateHandle, rememberSaveableKeptKeptLostSmall things the user chose: an id, a tab, a scroll position, a draft — under 50 KB
Disk — Room, DataStore, filesKeptKeptKeptAnything the user would mind losing

Testing rows 11–12 takes a minute. The developer option “Don’t keep activities” destroys every activity as soon as you leave it, which catches part of the problem. The real test is to put the app in the background, run adb shell am kill with its package name — it kills only a background process — and reopen the app from Recents. The screen should look exactly as you left it. Since Android 11, ApplicationExitInfo tells the new process why the last one ended — low memory, a crash, an ANR, the user — so a team can count process deaths in production instead of guessing.

Memory itself is now policed from two sides. Android 17, released on 16 June 2026, sets per-app memory limits based on the device’s total RAM and ends a process that goes past them; the exit reason carries the text “MemoryLimiter:AnonSwap”. Google Play has made memory usage a core vital with thresholds per RAM tier — on a 4 GB phone, 2 GB in the foreground and 1 GB in the background — and says apps above them may lose store visibility from February 2027. Images are the usual cause, for the reason the React Native page gives: decoded, an image costs width × height × 4 bytes, so a 12-megapixel photo is 48 MB in memory.

Kotlin and coroutines: work that ends with the screen

Kotlin is a language from JetBrains, stable since February 2016, that Google made an official Android language in May 2017 and its preferred one in 2019. Jetpack Compose exists only in Kotlin, and the documentation and new are written for it first. Kotlin compiles to the same as Java, which the Android build then turns into DEX code for the Android runtime, so the two languages call each other directly and a Java codebase can move one file at a time. Two features pay for themselves at once. Nullability is part of the type — a String? cannot be used as a String without a check — and data and sealed classes describe a screen’s states in a few lines, which the compiler then checks exhaustively in a when.

VersionReleasedWhat changed for an Android developer
1.0Feb 2016The first stable release; Google adopts it for Android in May 2017
1.3Oct 2018Coroutines become stable
1.9.20Nov 2023Kotlin Multiplatform stable — shared logic for Android and iOS
2.0May 2024The K2 compiler; the Compose compiler moves into the Kotlin repository and releases with it
2.0.20Aug 2024Strong skipping on by default in Compose
2.2Jun 2025Guard conditions in when stable; context parameters in preview
2.3Dec 2025A checker for ignored return values; Java 25 support
2.4Jun 2026Context parameters and explicit backing fields stable; collection literals (experimental); 18 months of security fixes per release line

Coroutines are what changed how Android code is written. A suspend function can pause — waiting for the network or the database — without blocking its thread, so asynchronous work reads from top to bottom. Dispatchers say where it runs: Dispatchers.Main for the , IO for blocking reads and writes, Default for computation. What makes coroutines safe rather than merely convenient is structured concurrency. Every coroutine is started in a scope, and when the scope is cancelled, everything started in it is cancelled too. The table at the top is therefore also a table of scopes: the right scope is the one that ends exactly when the work stops being useful.

Structured concurrency means every coroutine belongs to a scope, and cancelling the scope cancels everything in it. The scopes differ in when they end. `viewModelScope` ends when the ViewModel is cleared, so loading started there survives a rotation and stops when the user leaves for good. `lifecycleScope` belongs to the activity or fragment and is cancelled on every rotation. `GlobalScope` is never cancelled, which is how a request outlives its screen and delivers into nothing. WorkManager is not a scope: its work is written to disk and survives process death and reboot. Choose the scope that ends exactly when the work stops being useful.
ScopeCancelled whenUse it for
viewModelScopeThe ViewModel is cleared — it survives rows 5–10 and ends with rows 11–16Loading and refreshing a screen’s data
lifecycleScopeThe activity or fragment is destroyed — including on every rotationShort work tied to that one object, such as showing a dialog
rememberCoroutineScope()The composable leaves the compositionWork started by a tap — scrolling to the top, an animation
GlobalScopeNeverAlmost nothing: it is how work outlives the screen it was for
WorkManagerNot a scope — the work is persisted and survives rows 11–16Work that must finish even if the user leaves

On top of coroutines sits Flow, a stream of values over time: a database query that emits again when the rows change, a search field that queries as the user types. For screen state, StateFlow holds the current value and emits updates, which is what a rebuilt screen needs — something to read at once and to observe afterwards. How the screen collects it matters as much as where it is produced. collectAsStateWithLifecycle() in Compose, or repeatOnLifecycle(STARTED) with Views, stops collecting when the app goes to the background and starts again when it returns. Without it, a flow of location updates keeps the GPS running for a screen nobody is looking at.

The main thread has a deadline on Android too. If it does not handle an input event within 5 seconds, the system shows the “App isn’t responding” dialog — an ANR. Google Play counts the share of sessions that hit a user-perceived ANR, and above 0.47% overall, or 8% on any single phone model, the app’s visibility in the store can suffer. Most ANRs are ordinary work in the wrong place: a database query, a large parse or a file read on Dispatchers.Main. StrictMode, switched on in debug builds, reports disk and network access on the main thread as it happens.

Compose: three phases, and what recomposes

Jetpack Compose, stable since July 2021 and at version 1.12 in September 2026, is the UI toolkit Google recommends. You write composable functions that describe the screen for a given state; when a state they read changes, Compose runs them again — recomposition — and updates what changed. It replaces XML layouts, findViewById, adapters and the habit of holding references to views and changing them by hand. For a list, it is the difference between a RecyclerView with an adapter and a view holder, and a LazyColumn with a lambda.

Compose builds each frame in three phases. Composition decides what to show by running composable functions. Layout decides where: every element is measured and placed. Drawing renders the pixels. Compose records which phase read each piece of state, and when the state changes it repeats only that phase and the ones after it. That gives the main performance rule a precise form: read state as late as you can. A header that moves with the scroll and reads the scroll offset in its composable recomposes on every frame of the scroll. The same header reading the offset inside Modifier.offset { } repeats only layout, and inside graphicsLayer { } only drawing.

Compose produces each frame in three phases: composition runs composable functions to decide what to show, layout measures and places each element, and drawing renders the pixels. It records which phase read each piece of state and, when the state changes, repeats only that phase and the ones after it. So a value that changes every frame — a scroll offset, an animation — should be read as late as possible: in the layout lambda of `Modifier.offset`, or in the draw lambda of `graphicsLayer`, rather than in the composable's body, where it reruns composition sixty or more times a second.

The second rule is skipping. During recomposition Compose skips a composable whose parameters have not changed. Since Kotlin 2.0.20 strong skipping is on by default: parameters of stable types are compared with equals(), and everything else by identity — is it the same object? That makes one pattern expensive in a way that is hard to see. Room emits a new list of new objects whenever any row in a query changes, so every row of a LazyColumn receives a different object and recomposes, even if only one row changed. Give list items stable keys, mark data classes that really are unchanging with @Immutable, and read the Compose compiler’s stability report before guessing.

ConcernViews (XML)Compose
How state reaches the screenYou find the view and set the value, and keep the two in sync yourselfThe screen is a function of state; there is nothing to keep in sync
ListsRecyclerView, an adapter and a view holder — several files per listLazyColumn with an item lambda, virtualised by default
Where it bitesLeaked references to views after row 2, state that drifts from the screenState read too early, unstable parameters, new objects from the data layer
Measuring itLayout Inspector, GPU rendering profileRecomposition counts in Layout Inspector, the compiler’s stability report
Reality in 2026Most existing screens, and libraries that still expect itWhere new work goes; the two interoperate in both directions

Compose also carries the platform’s changes in screen shape. Since apps were required to target Android 15, they draw edge to edge, under the status and navigation bars; for apps targeting Android 16 the opt-out is gone, the back gesture plays a predictive animation by default, and on screens 600 dp and wider the system ignores orientation locks — row 9. A layout written for one phone held upright now has to work on a tablet, an unfolded foldable and a resizable window. Window size classes and the adaptive Compose libraries exist for exactly that, and the React Native page lists the same deadlines from a cross-platform team’s side.

Background work: what the system lets you run

An app that is not on screen has, by default, no right to keep running, and almost every Android release since 2015 has narrowed what it may do. Doze in Android 6.0 deferred network access and jobs while the phone lies still. Android 8.0 limited background services. Android 9 sorted apps into standby buckets by how recently they were used. Android 14 required every foreground service to declare a type and stopped granting exact alarms to newly installed apps by default. Android 15 capped data-sync services at six hours a day. On top of that, several manufacturers add battery managers of their own that end background work more aggressively than stock Android; the site dontkillmyapp.com tracks them by brand.

Every Android release since 2015 has narrowed what an app may do off screen — Doze, background limits, standby buckets, typed foreground services, time limits on data sync — and several manufacturers restrict it further. So “run it in the background” is a choice between tools the system will actually honour, and the order of the questions matters: most work belongs to the screen and should stop with it; only work the user is watching earns a foreground service and its notification; an exact time needs an exact alarm and its permission; and everything that must eventually finish goes to WorkManager, which persists it and runs it when the system allows.
The jobWhat to useWhat the system promises
Work for the screen the user is looking atA coroutine in viewModelScopeIt runs while the screen exists and stops with it — nothing more is needed
Work that must finish eventually — a sync, an upload of logsWorkManager, with constraints such as “on Wi-Fi” or “while charging”It survives process death and reboot and runs when the system allows; periodic work at most every 15 minutes
Something the user started and is watching — navigation, a call, music, a video uploadA foreground service with a declared type and a visible notificationIt runs while the notification shows; on Android 15, data-sync and media-processing services stop after six hours in a day
Something at an exact time the user set — an alarm clock, a calendar reminderAn exact alarm from AlarmManagerIt fires on time even in Doze, but since Android 14 the permission is not granted to new installs by default
Keeping the processor awake while the screen is offA partial wake lock — briefly, and rarelyNothing: since 1 March 2026 Google Play may demote an app that holds one too long

The practical consequence is that “run in the background” is never a requirement you can accept as written. Ask what the user needs to happen and when they need to know it happened. Most answers are WorkManager with a constraint, a foreground service with a notification, or a push message from the server that tells the app there is something to fetch. Test every background path on two or three phones from manufacturers known for aggressive battery management, because the stock emulator is the one place where background work always runs.

Shipping: tracks, staged rollouts and the Play calendar

Google Play gives every app four tracks. Internal testing reaches up to 100 testers, usually within seconds of a build being added. Closed and open testing reach larger groups, and production reaches everyone. A personal developer account created after 13 November 2023 must run a closed test with at least 12 testers, opted in for 14 days in a row, before it may publish to production at all. Review is usually faster than Apple’s, but it exists, and policy checks — permissions declared, the data-safety form matching what the app does, the target API level — are automated and do not negotiate.

A staged rollout releases an update to a percentage of users you choose and raise over time — 1%, then 5%, then 20% — and can be halted at any point. While it runs, Android vitals in the Play Console shows the new version’s crash and ANR rates against the thresholds that affect the app’s visibility: a user-perceived crash rate of 1.09% overall or 8% on one phone model, an ANR rate of 0.47% overall or 8% on one model. The per-model threshold is the one that catches fragmentation: a crash on one manufacturer’s phones can be invisible in the overall average and still cost the app its listing on those phones.

Google Play’s calendar — the requirements around this page’s date

SinceWhat it requiresWhat it means in the code
Aug 2021New apps publish as Android App BundlesPlay builds the APK for each device and signs it with Play App Signing
1 Nov 2025 (extensions to 31 May 2026)Apps targeting Android 15 or later support 16 KB memory pagesEvery native library in every dependency rebuilt with 16 KB alignment
1 Mar 2026Excessive partial wake locks affect store visibilityWake locks released on every path, including those taken by SDKs
31 Aug 2026 (extensions to 1 Nov 2026)New apps and updates target Android 16 (API 36); existing apps below API 35 are hidden from new users on newer AndroidNo opt-out from edge-to-edge, predictive back by default, orientation locks ignored on large screens — rows 9 and 13
30 Sep 2026Developer verification in Brazil, Indonesia, Singapore and Thailand, for apps from participating stores on certified devicesAn app distributed outside Play still needs a verified developer behind it; Google says the rule goes global in 2027
Feb 2027Memory usage, bitmap memory and code optimisation may affect store visibilityMemory measured against your users’ RAM tiers; R8 enabled
Targeting Android 17 (API 37)Next year’s target if the yearly pattern holdsNo orientation opt-out on large screens, a runtime permission for the local network, static final fields locked against reflection

Which Android codebase are you looking at

What you see in the codeWhat it tells you
android:configChanges="orientation|screenSize" on most activitiesSomeone silenced rows 5 and 9 instead of saving state; rows 11–12 still lose it
android:screenOrientation="portrait"Ignored on screens 600 dp and wider for apps targeting API 36; no opt-out from API 37
override fun onBackPressed()Not called for apps targeting API 36; use OnBackPressedCallback
GlobalScope.launchWork nobody cancels — it outlives the screen it was started for
AsyncTaskDeprecated since Android 11; code from before coroutines
kotlinCompilerExtensionVersion in build.gradleThe Compose compiler set up as before Kotlin 2.0; it now ships with Kotlin as a Gradle plugin
LiveData, XML layouts and findViewByIdThe View era; it works, and new screens go to Compose with StateFlow
targetSdk below 35Invisible to new users on newer Android since 31 Aug 2026

Five cases, and what each one was missing

Android’s cases share one shape: something the app does not control — the system, a shared component, a manufacturer, another app — changes underneath it. Each is documented by the people involved: a security firm’s disclosure, Google’s own documentation and statements, the companies whose users were affected.

CaseWhat happenedWhat was missing
Stagefright, Jul–Aug 2015Bugs in Android’s media library, reachable by a single MMS, exposed an estimated 950 million devicesAn update path that did not wait for every manufacturer and carrier
Android 7.0, Aug 2016Oversized saved state, until then silently dropped, began to crash apps targeting 7.0Saved state kept small — ids, not data
Google Home, 2020New features written in Kotlin; NullPointerExceptions, the most common crash type in Play Console, fell by 33%Nothing — the type system did the work
Android System WebView, 22 Mar 2021An automatic WebView update crashed Gmail and many other apps that had shipped nothingCrash monitoring that splits by the version of a system component, not only the app’s own
Teams and emergency calls, Dec 2021Teams, installed but signed out, registered phone accounts again and again; on some Pixel phones, 911 calls failedA platform API used in a loop no test covered, on the one path nobody can test in production

The WebView case shows what that means in practice. On 22 March 2021 an update to Android System WebView — the component that draws web content inside other apps, updated through Google Play — reached phones automatically, and apps across the ecosystem began to crash: Gmail, the Google app, Amazon, Discord, Outlook, banking apps. None of them had shipped anything. Google confirmed the problem within hours and released WebView and Chrome 89.0.4389.105, and users were told to update both. For an app team, the lesson is in monitoring: a crash spike with no release behind it is a system component until proven otherwise, and the crash reports have to carry the WebView version to show it.

The fifth case is the most serious and the most instructive. In December 2021 a user reported that a 911 call from a Pixel phone had failed: the phone showed a call, rang once and connected to nothing. Google traced it to an unintended interaction with Microsoft Teams. When Teams was installed but the user was not signed in, it registered a phone account with Android’s telecom service again and again, and on some Pixel phones the duplicates broke the emergency call path. Microsoft released a fixed Teams within days, and Google shipped its own platform fix in the January 2022 update. A platform API called in a loop is an ordinary bug. The lesson is that an app’s calls into shared system services reach paths the app’s own tests cannot — and an emergency call is a path nobody can try in production.

Where it degrades

Android is generous to a team that designs for the system’s ownership of the app and unforgiving to one that assumes the process stays alive. The language and the toolkit are good; the failures come from the events in the table and from the phones the team does not own. Almost every failure is one of these:

  • Screen state held in the activity, which empties on the first rotation (rows 5–10).
  • Nothing in saved state, so the app comes back blank after the system reclaims memory (rows 11–12).
  • Too much in saved state, so it crashes on the way into the background.
  • configChanges used to silence rotation instead of saving state.
  • Work in GlobalScope, and coroutines that swallow their own cancellation.
  • Flows collected without regard to the lifecycle, keeping sensors and the network running in the background.
  • State read in composition that could be read in layout or drawing, and new objects from the data layer on every emission.
  • Background work assumed to run, wake locks held on failure paths, and no test on an aggressive manufacturer’s phone.
  • Testing only on a flagship and an emulator, which hides every memory and frame problem your users actually have.
  • Play’s calendar — target API, 16 KB pages, vitals thresholds — discovered when an update is rejected.

The honest limit is reach without control. Android runs on more kinds of devices than any other system, from 2 GB phones to foldables and desktop windows, and that reach is the reason to build for it; in much of the world it is where most users are. The same reach means the app never knows the device it runs on, the system reclaims it whenever it likes, and parts of the platform beneath it update on someone else’s schedule. No architecture removes that. A good one makes every event in the table survivable, measures what happens on the phones the team does not own, and treats Play’s yearly target as a fixed item in the plan.

The practical check takes ten minutes. Read targetSdk, minSdk and the Kotlin version, and find them in the calendar table. Search for configChanges, GlobalScope, onBackPressed, catch (e: Exception) around suspending calls, and .collect { outside repeatOnLifecycle. Put the app in the background on the busiest screen, run adb shell am kill, and reopen it from Recents. Rotate every screen once. Open Android vitals and read the crash and ANR rates per phone model, not only overall. And check which of your SDKs take wake locks.

When to use it

Use it when

  • Products whose users are mostly on Android — in much of the world that is the great majority of them.
  • Apps that need deep system integration — background sync, widgets, sharing, custom keyboards, accessibility services.
  • Anything that must run acceptably on low-end hardware, where a native build has room the alternatives do not.
  • Teams already on the JVM — Kotlin is a short move, and Kotlin Multiplatform can later share the logic with iOS.

Avoid it when

  • Building both platforms natively with one small team, which doubles the surface without doubling the people.
  • Designs that assume a fixed screen size or orientation — the platform now overrides both on large screens.
  • Architectures that assume the process stays alive, which is the assumption Android specifically does not honour.
  • Products that cannot commit to the yearly target API upgrade, which is a hard deadline on this platform.

Sources and method

What this page stands on

  • 15 sources
  • 3 documented failures
  • Checked October 13, 2026

Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.

Found this useful?

Share it with someone who is working on the same problem.