Process Death verstehen: So überleben deine Screens den System-Kill
Lerne, wie Android Prozesse beendet, warum saved state entscheidend ist und wie du resiliente Screens baust.
Stell dir vor, eine Nutzerin füllt in deiner App ein längeres Formular aus, wechselt kurz in den Browser, um eine Information nachzuschlagen, und kehrt zwanzig Minuten später zurück. Sie erwartet, dass sie genau dort weitermacht, wo sie aufgehört hat. Stattdessen sieht sie ein leeres Formular, weil Android in der Zwischenzeit den Prozess deiner App beendet hat. Genau diese Situation beschreibt das Phänomen Process Death, und sie ist kein seltener Sonderfall, sondern Alltag auf Geräten mit begrenztem Arbeitsspeicher. Wenn du robuste Apps bauen willst, musst du verstehen, wann Android deinen Prozess kassiert, welche Daten du retten musst und welche APIs dir Jetpack und Compose dafür zur Verfügung stellen.
Was ist das?
Process Death bezeichnet den Moment, in dem das Android-Betriebssystem den Linux-Prozess deiner App vollständig beendet, obwohl der Nutzer die App nicht selbst geschlossen hat. Android verwaltet eine Liste laufender Prozesse und sortiert sie nach Wichtigkeit: Vordergrund-Apps stehen oben, gefolgt von sichtbaren, dann von Hintergrund-Apps und ganz unten von leeren Prozessen, die nur noch im Cache liegen. Sobald das System Speicher für eine andere App benötigt, beginnt es am unteren Ende der Liste mit dem Aufräumen. Dein Prozess wird dabei einfach gestoppt; jede Variable im Heap, jede laufende Coroutine, jede Singleton-Instanz verschwindet. Beim nächsten Start ist dein Code-Heap leer wie nach einem Kaltstart.
Wichtig ist die Abgrenzung zu zwei verwandten Vorgängen, die oft verwechselt werden. Erstens ist da die Konfigurationsänderung, etwa eine Bildschirmrotation: Die Activity wird zerstört und neu erzeugt, der Prozess läuft aber weiter, und ein ViewModel überlebt den Vorgang. Zweitens gibt es den klassischen Backstack-Wechsel, bei dem nur eine einzelne Activity in den Hintergrund gerät. Bei Process Death dagegen geht der gesamte Prozess verloren, inklusive ViewModels, statischer Felder und in-memory Caches. Wenn der Nutzer später per Recents-Liste zurückkehrt, ruft Android genau die Activity wieder auf, die zuletzt aktiv war, und erwartet von dir, dass die UI dort weiterläuft, wo sie aufgehört hat. Aus Nutzersicht ist die App nie weg gewesen, aus Sicht deines Codes beginnt aber alles neu.
Im Roadmap-Kontext ist Process Death der Stresstest für jede Architekturentscheidung, die du triffst. Die Trennung zwischen UI-State, Domain-State und Persistenz ist nur dann sauber, wenn deine Screens nach einem Kill so wieder hochfahren, dass die Nutzerin den Unterschied nicht merkt. Das ist die Definition von Resilienz auf Android: Dein Bildschirm ist nicht das, was du gerade renderst, sondern das, was sich aus den gespeicherten Daten jederzeit wieder herstellen lässt.
Wie funktioniert es?
Damit du den richtigen Speichermechanismus wählen kannst, lohnt sich ein Blick auf die drei Ebenen, die Android dir zur Verfügung stellt. Auf der untersten Ebene liegt die Persistenz: SharedPreferences, DataStore, Room oder ein Backend. Diese Daten überleben jeden Prozess-Kill und sogar einen Reboot. Auf der mittleren Ebene gibt es den SavedState, ein kleines Bundle, das Android beim Beenden des Prozesses für dich aufhebt und beim Wiederstart zurückliefert. Auf der obersten Ebene liegt der reine In-Memory-State in ViewModels, Composables und Singletons; er stirbt mit dem Prozess.
Der entscheidende Mechanismus ist der SavedState. Sobald deine Activity in den Hintergrund geht, ruft Android onSaveInstanceState auf und gibt dir ein Bundle, in das du primitive Werte und Parcelables schreiben darfst. Dieses Bundle wird vom System aufbewahrt, auch nachdem dein Prozess gekillt wurde. Wenn der Nutzer zurückkehrt, startet Android deine Activity neu und reicht dasselbe Bundle in onCreate und onRestoreInstanceState wieder hinein. Diese Vertragsschicht ist die Brücke über den Kill-Moment hinweg.
In modernen Apps berührst du onSaveInstanceState selten direkt. Stattdessen arbeitest du mit SavedStateHandle im ViewModel. Das ist eine Map-ähnliche Struktur, die du im Konstruktor injiziert bekommst. Werte, die du dort ablegst, fließen automatisch ins Bundle, und beim Wiederstart liest du sie genauso wieder aus. Das Schöne daran: Du musst dich nicht mehr um Lifecycle-Callbacks kümmern, sondern arbeitest deklarativ. Ein typisches Muster ist savedStateHandle.getStateFlow("query", ""), was dir einen Flow liefert, der seinen letzten Wert auch nach Process Death behält.
In Jetpack Compose gibt es zwei verwandte APIs, die du sauber auseinanderhalten musst. remember speichert einen Wert nur über Recompositions hinweg; bei einer Konfigurationsänderung oder einem Process-Kill ist er verloren. rememberSaveable schreibt den Wert zusätzlich in den SavedState und überlebt damit beide Vorgänge. Für eigene Datenklassen brauchst du dabei einen Saver, der dem Framework erklärt, wie der Typ in ein Bundle übersetzt wird. Die offizielle Dokumentation zu State in Compose nennt das eine der wichtigsten Designentscheidungen für jede Composable: Was muss überleben, was darf weg.
Eine Regel hilft dir bei der Einordnung. Frag dich bei jedem State-Holder: Wenn der Nutzer jetzt das Telefon hinlegt, eine Stunde Filme schaut und zurückkommt, was muss noch da sein? Eingaben in einem Formular gehören in den SavedState. Eine geladene Liste von der API gehört nicht hinein, denn sie kann aus der Datenbank oder dem Netzwerk wieder geholt werden. Eine offene Animation ist reiner UI-State und darf verloren gehen. Diese drei Schubladen, sauber getrennt, ergeben eine Architektur, die einen Kill aushält.
In der Praxis
Schauen wir uns ein konkretes Beispiel an: einen Suchbildschirm mit Texteingabe und Ergebnisliste. Die Sucheingabe ist Nutzer-State und gehört in den SavedState; die Ergebnisliste ist abgeleiteter Daten-State und kommt aus dem Repository.
class SearchViewModel(
private val savedStateHandle: SavedStateHandle,
private val repository: ArticleRepository,
) : ViewModel() {
val query: StateFlow<String> = savedStateHandle.getStateFlow(KEY_QUERY, "")
val results: StateFlow<List<Article>> = query
.debounce(300)
.flatMapLatest { current ->
if (current.isBlank()) flowOf(emptyList())
else repository.search(current)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
fun onQueryChange(newValue: String) {
savedStateHandle[KEY_QUERY] = newValue
}
private companion object {
const val KEY_QUERY = "query"
}
}
Das ViewModel hält die Eingabe konsequent in SavedStateHandle. Selbst wenn Android den Prozess beendet, während der Nutzer auf einem Suchergebnis-Screen ist, kommt der zuletzt eingegebene Suchbegriff nach dem Wiederstart automatisch zurück, und die Ergebnisliste wird neu geladen. Auf der Compose-Seite reicht eine schlanke Bindung:
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
val query by viewModel.query.collectAsStateWithLifecycle()
val results by viewModel.results.collectAsStateWithLifecycle()
Column {
TextField(value = query, onValueChange = viewModel::onQueryChange)
ResultsList(results)
}
}
Wenn du dagegen einen lokalen UI-State brauchst, etwa den Aufklapp-Zustand eines Filtermenüs, nutzt du im Composable selbst rememberSaveable:
var filtersExpanded by rememberSaveable { mutableStateOf(false) }
Damit überlebt der Zustand sowohl die Bildschirmrotation als auch einen Process-Kill, ohne dass du im ViewModel Platz dafür schaffen musst.
Eine typische Stolperfalle
Die häufigste Falle ist die Annahme, dass ein ViewModel mit viewModelScope und ein paar MutableStateFlow-Feldern automatisch sicher ist. ViewModels überleben Konfigurationsänderungen, aber nicht Process Death. Wenn du den Suchbegriff in MutableStateFlow("") hältst, statt im SavedStateHandle, ist er nach dem System-Kill weg, obwohl die App auf den ersten Blick „normal” weiterläuft. Genauso gefährlich sind Singletons im DI-Graph, in denen Anwendungs-State liegt: Sie werden nach dem Wiederstart frisch gebaut, und alles, was nicht aus persistenter Quelle nachfließt, ist verloren.
Eine zweite Falle betrifft die Datenmenge. Der SavedState liegt in einem Bundle, das per Inter-Process-Communication zum System wandert, und ist auf etwa 500 KB begrenzt, in der Praxis solltest du deutlich darunter bleiben. Lege dort niemals ganze Listen, Bilder oder API-Responses ab. Speichere stattdessen einen Schlüssel oder eine ID, mit der du beim Wiederstart die echten Daten aus Room oder dem Netzwerk neu beziehst.
Validierung im Test
Um zu prüfen, ob deine Screens Process Death wirklich überstehen, gibt es einen verlässlichen Workflow. Aktiviere in den Entwickleroptionen die Einstellung „Activities nicht behalten” oder nutze das ADB-Kommando adb shell am kill <package>, während die App im Hintergrund liegt. Anschließend kehrst du über die Recents-Liste zurück und beobachtest, ob alle Eingaben, gewählten Filter und Scroll-Positionen, die der Nutzer erwarten würde, wieder da sind. Ergänze diesen manuellen Test um Instrumented Tests, die ActivityScenario.recreate() und gezieltes Töten des Prozesses verwenden. In Compose kannst du außerdem mit StateRestorationTester einen Recompose-Zyklus simulieren, der den SavedState durchläuft. Wenn ein Element nach dieser Prozedur seinen Zustand verliert, weißt du sofort, welcher State-Holder noch nicht resilient ist.
Fazit
Process Death ist kein exotischer Fehlerfall, sondern eine Grundannahme des Android-Lebenszyklus. Sobald du verinnerlicht hast, dass jeder Bildschirm jederzeit aus dem Nichts neu hochfahren können muss, fallen viele Architekturentscheidungen leichter. Du trennst flüchtigen UI-State im Speicher, kritischen Nutzer-State im SavedState und dauerhafte Daten in der Persistenzschicht. Du nutzt SavedStateHandle im ViewModel und rememberSaveable in Composables, um die Brücke über den Kill-Moment zu bauen, und du verlässt dich nicht darauf, dass Singletons den Prozess überleben. Bevor du das nächste Feature als fertig erklärst, schalte „Activities nicht behalten” ein, klicke dich durch den Flow und prüfe per Code-Review, ob jeder relevante State entweder im SavedState liegt oder reproduzierbar nachgeladen wird. Erst wenn dein Screen nach einem erzwungenen Prozess-Kill so wirkt, als wäre nie etwas passiert, ist deine UI wirklich resilient.