20 häufig gestellte Android-Interviewfragen. Android-Entwicklung ist ein gefragtes Feld für mobile Apps auf Kotlin-Basis, Android SDK und Jetpack sowie Lifecycle-, Performance-, Netzwerk-, Storage-, Test- und Release-Aufgaben. Die Fragen decken verschiedene Niveaus ab, und du kannst mündliche Antworten in unserem Interview-Trainer üben.
1Beschreiben Sie die zentralen Lebenszyklus-Callbacks einer Activity von der Erstellung bis zum Abbau. Wie unterscheiden sich onPause(), onStop() und onDestroy() hinsichtlich der Ressourcenfreigabe?
Die sechs zentralen Callbacks sind onCreate(), onStart(), onResume(), onPause(), onStop() und onDestroy(). onCreate() übernimmt die einmalige Initialisierung, etwa das Erzeugen der Ansichten aus Layoutdefinitionen. onStart() macht die Activity sichtbar, und onResume() führt sie in den Zustand Resumed. Fensterfokus und Lebenszykluszustand sind voneinander zu unterscheiden: Im modernen Mehrfenstermodus können mehrere Activities zugleich im Zustand Resumed sein. onPause() signalisiert das Verlassen dieses Zustands, nicht lediglich eine Änderung des Fensterfokus. onStop() zeigt an, dass die Activity nicht mehr sichtbar ist, und onDestroy() kennzeichnet den abschließenden Abbau der Instanz. In onPause() sollten nur schnell abschließbare, auf den Vordergrund bezogene Operationen pausiert werden, etwa Animationen der Benutzeroberfläche oder eine leichtgewichtige Kameravorschau. Längere Arbeit kann beim Activity-Wechsel das Fortsetzen der nächsten Activity verzögern. onStop() ist der zentrale Ort zur Freigabe aufwendigerer Ressourcen, die an die Sichtbarkeit der Benutzeroberfläche gebunden sind, etwa Standort- oder Sensorlistener, Medienwiedergabe oder regelmäßige Netzwerkabfragen. Beabsichtigte Wiedergabe oder Ortung im Hintergrund braucht eine geeignete unabhängige Zuständigkeit und muss Androids Regeln zur Hintergrundausführung beachten. Das System kann einen Hintergrundprozess ohne weitere Benachrichtigung beenden. onDestroy() dient dem abschließenden Aufräumen der Activity-Instanz, etwa von lokalen Threads und Referenzen. Wichtige Ressourcen dürfen nicht erst dort freigegeben werden, weil Android einen Prozess mit gestoppter Activity zur Speicherfreigabe beenden kann, ohne onDestroy() aufzurufen.
class LocationActivity : AppCompatActivity() {
private var locationClient: LocationClient? = null
override fun onStart() {
super.onStart()
// Start listening when visible to the user
locationClient?.startLocationUpdates()
}
override fun onStop() {
super.onStop()
// Release when invisible to save battery and prevent leaks on process kill
locationClient?.stopLocationUpdates()
}
}
2Was sind die vier primären Android-Anwendungskomponenten und wie sieht das Lebenszyklus- und Threading-Modell ihrer Standard-Callbacks aus?
Die vier primären Android-Anwendungskomponenten sind Activity, Service, BroadcastReceiver und ContentProvider. Eine Activity stellt eine Benutzeroberfläche für Bildschirminteraktionen bereit. Ein Service führt Hintergrundverarbeitung ohne dedizierte Benutzeroberfläche aus. Ein BroadcastReceiver lauscht auf systemweite oder anwendungsbezogene Broadcast-Benachrichtigungen und reagiert auf diese. Ein ContentProvider verwaltet und exponiert strukturierte Daten für andere Anwendungen oder interne Module. Standardmäßig werden die primären Lebenszyklus-Callbacks für Activity (wie onCreate, onStart, onResume), Service (wie onCreate, onStartCommand, onBind) und BroadcastReceiver (onReceive) synchron auf dem Haupt-Thread (UI-Thread) der Anwendung ausgeführt. ContentProvider-Methoden (wie onCreate) initialisieren sich ebenfalls auf dem Haupt-Thread, wenngleich prozessübergreifend aufgerufene Abfrage- oder Einfügeoperationen auf Threads des Binder-Threadpools laufen. Da Standard-Callbacks auf dem Haupt-Thread ausgeführt werden, blockiert das direkte Ausführen schwerer, blockierender Operationen wie Netzwerkanfragen oder umfangreicher Datenbank-Festplatten-I/O die Benutzeroberflächen-Ereignisschleife und löst einen ANR-Fehler (Application Not Responding) aus.
class DataSyncService : Service() {
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// onStartCommand runs on the Main Thread; heavy work must be offloaded
serviceScope.launch {
performDiskAndNetworkSync()
stopSelf(startId)
}
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
}
private fun performDiskAndNetworkSync() {
// Blocking I/O safely executed on Dispatchers.IO
}
}
3Welche Hauptaufgaben hat ein ViewModel in Androids MVVM-Architektur (Model-View-ViewModel) im Vergleich zu UI-Controllern (User Interface) wie Activities und Fragments, und warum sollte man keinen Context an ein ViewModel übergeben?
In Androids MVVM-Architektur ist das ViewModel dafür verantwortlich, UI-bezogenen Zustand zu halten und zu verwalten, die Präsentationslogik zu steuern und Konfigurationsänderungen (wie Bildschirmdrehungen) zu überstehen. Im Gegensatz dazu sind UI-Controller (Activities und Fragments) dafür zuständig, UI-Elemente auf dem Bildschirm darzustellen, Zustandsänderungen zu beobachten, direkte Benutzerinteraktionen (Klicks, Gesten) zu verarbeiten und Android-Lebenszyklusereignisse zu verwalten. Von der Übergabe eines Activity-Context oder einer View-Referenz an ein ViewModel wird dringend abgeraten, da ViewModels den Lebenszyklus von UI-Controllern über Konfigurationsänderungen hinweg in der Regel überdauern. Wenn eine Activity bei einer Bildschirmdrehung zerstört und neu erstellt wird, verhindert ein ViewModel, das ihren Context hält, dass die alte Activity von der Garbage Collection bereinigt werden kann, was zu einem Speicherleck führt. Falls ein Android-Context für systemnahe Operationen zwingend erforderlich ist, sollte stattdessen ein Context mit Anwendungs-Geltungsbereich (beispielsweise über `AndroidViewModel` oder durch die Injektion des ApplicationContext) verwendet werden.
// Bad: Storing Activity context or View reference causes leaks
class LeakyViewModel(private val activityContext: Context) : ViewModel() {
fun showToast() { Toast.makeText(activityContext, "Hello", Toast.LENGTH_SHORT).show() }
}
// Good: ViewModel manages state; Activity observes and handles context-specific tasks
class SafeViewModel(private val repository: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
_uiState.value = UserUiState.Success(repository.getUser(id))
}
}
}
4Wie unterscheiden sich normale und gefährliche Android-Berechtigungen (Dangerous Permissions), und was geschieht, wenn eine App eine gefährliche Berechtigung zur Laufzeit anfordert?
Normale Berechtigungen stellen ein minimales Risiko für die Privatsphäre der Nutzer oder den Gerätebetrieb dar und werden vom System bei der Installation der App automatisch gewährt, wenn sie im `AndroidManifest.xml` deklariert sind (wie z. B. `ACCESS_NETWORK_STATE`). Gefährliche Berechtigungen (auch Laufzeitberechtigungen oder Runtime Permissions genannt) regeln den Zugriff auf private Nutzerdaten oder eingeschränkte Gerätefunktionen wie Kamera, Mikrofon, Standort und Kontakte. Wenn eine App eine gefährliche Berechtigung zur Laufzeit anfordert, zeigt das Android-System einen Systemdialog an, der den Nutzer auffordert, den Zugriff zu erlauben oder zu verweigern. Erteilt der Nutzer die Berechtigung, führt die App die angeforderte Funktion aus. Verweigert der Nutzer die Berechtigung, erhält die App eine Callback-Benachrichtigung über die Ablehnung und muss dies sauber abfangen, indem sie die abhängige Funktion deaktiviert oder einschränkt, ohne abzustürzen, und optional eine erklärende Begründung in der Benutzeroberfläche anzeigt.
5Was ist der Unterschied zwischen Debug- und Release-Builds unter Android, und wie verbinden sich Build Types und Product Flavors zu Build Variants?
In der Android-Entwicklung konfigurieren Build Types (Build-Typen) Paketierungs- und Laufzeiteigenschaften für verschiedene Phasen der Entwicklung. Debug-Builds aktivieren Debugging-Flags (`isDebuggable = true`), verwenden einen standardmäßigen Debug-Keystore und deaktivieren in der Regel Code-Minifizierung und Shrinking (R8/ProGuard) für schnelle Build-Iterationen. Release-Builds deaktivieren Debugging-Flags, aktivieren Optimierungen sowie Code-Verschleierung (Obfuskation) und erfordern einen sicheren Release-Signaturschlüssel für die Verteilung. Product Flavors stellen verschiedene Versionen oder Zielvarianten der App dar, die dieselbe Codebasis teilen, wie etwa unterschiedliche Umgebungen (Staging vs. Produktion) oder Produkt-Editionen (kostenlos vs. kostenpflichtig). In Gradle entstehen Build Variants durch das kartesische Produkt aus Product Flavors und Build Types (Build Variant = Product Flavor × Build Type). Die Kombination der Flavors `staging` und `production` mit den Build Types `debug` und `release` ergibt beispielsweise vier Build-Varianten: `stagingDebug`, `stagingRelease`, `productionDebug` und `productionRelease`.
6Was ist eine Coroutine in Kotlin, und wie unterscheidet sich das Suspendieren vom Blockieren eines Threads zur Laufzeit unter Android?
Eine Coroutine in Kotlin ist eine leichtgewichtige Abstraktion für Nebenläufigkeit, die es ermöglicht, asynchronen, nicht-blockierenden Code sequenziell zu schreiben. Mehrere Coroutines können gleichzeitig auf einem einzelnen Thread oder über Threadpools hinweg ausgeführt werden, ohne den hohen Speicher- und Kontextwechsel-Overhead von Threads auf Betriebssystemebene zu verursachen. Der wesentliche Unterschied zwischen Suspendieren und Blockieren liegt in der Thread-Nutzung: - Blockieren (z. B. durch Thread.sleep() oder synchrone Festplatten-/Netzwerk-I/O) hält den zugrunde liegenden Thread an und blockiert ihn für jede andere Arbeit. Auf dem Android-Haupt-Thread (Main Thread) friert ein Blockieren die Benutzeroberfläche ein und löst ANR-Fehler (Application Not Responding) aus. - Suspendieren (über suspend-Funktionen wie delay()) pausiert die Coroutine, ohne den zugrunde liegenden Thread anzuhalten. Die Coroutine speichert ihren Ausführungsstatus in einer Continuation und gibt den Thread an die Laufzeitumgebung zurück, damit dieser andere Aufgaben (wie UI-Interaktionen) ausführen kann. Sobald die asynchrone Operation abgeschlossen ist, wird die Coroutine fortgesetzt.
suspend fun fetchUserData(): String {
delay(1000) // Suspends execution and yields the thread
return "User Data"
}
fun fetchUserDataBlocking(): String {
Thread.sleep(1000) // Blocks and locks the underlying thread
return "User Data"
}
7Was ist in Jetpack Compose der Unterschied zwischen Zuständen, die mit remember versus rememberSaveable beibehalten werden, und wie steuert mutableStateOf UI-Aktualisierungen (User Interface) über Rekompositionen hinweg?
In Jetpack Compose speichert `remember` ein Objekt über Rekompositionen hinweg im Speicher, solange das Composable im Kompositionsbaum verbleibt. Allerdings geht ein Zustand, der ausschließlich mit `remember` gehalten wird, bei Konfigurationsänderungen (wie Bildschirmdrehungen) und durch das System initiierten Prozessbeendigungen verloren. Im Gegensatz dazu behält `rememberSaveable` den Zustand über Konfigurationsänderungen und Prozessbeendigungen hinweg bei, indem Werte automatisch über das Android Saved Instance State Bundle (oder über benutzerdefinierte `Saver`-Implementierungen für nicht standardmäßig unterstützte Datentypen) gespeichert und wiederhergestellt werden. `mutableStateOf` kapselt einen Wert in einem beobachtbaren `MutableState`-Objekt, das durch das Snapshot-Zustandssystem von Compose gestützt wird. Wenn ein Composable während der Komposition den Wert eines `MutableState` liest, zeichnet Compose diese Leseoperation auf. Ändert sich der Wert später, markiert Compose automatisch alle Composables, die diesen Wert gelesen haben, als ungültig und plant sie für eine Rekomposition ein, wodurch die UI stets mit dem aktualisierten Zustand synchron bleibt.
@Composable
fun CounterExample() {
// Resets to 0 on screen rotation
var localCount by remember { mutableStateOf(0) }
// Persists across screen rotation and process death
var persistentCount by rememberSaveable { mutableStateOf(0) }
Column {
Button(onClick = { localCount++; persistentCount++ }) {
Text("Local: $localCount | Saved: $persistentCount")
}
}
}
8Wie hilft die Null-Sicherheit in Kotlin dabei, Abstürze in Android-Apps zu verhindern, und wann verwendet man einen Nullable-Typ gegenüber einem Non-Null-Typ?
Die Null-Sicherheit in Kotlin hilft dabei, viele Android-Abstürze zu verhindern, indem Nullbarkeit zu einem festen Bestandteil des Typsystems gemacht wird. Ein als `String` deklarierter Wert wird als Non-Null behandelt, sodass der Compiler direkte Zugriffe wie `name.length` erlaubt. Ein als `String?` deklarierter Wert kann `null` sein, weshalb Kotlin dazu zwingt, diesen Fall vor der Verwendung zu behandeln. Dies reduziert versehentliche `NullPointerException`s durch optionale Intent-Extras, API-Antworten, Bundle-Argumente oder Java-/Plattform-APIs. Verwenden Sie einen Non-Null-Typ, wenn der Wert erforderlich ist, damit das Objekt oder die Funktion gültig ist. Verwenden Sie einen Nullable-Typ, wenn das Fehlen eines Werts ein tatsächlich erwarteter Zustand ist, wie beispielsweise eine optionale Profilbild-URL oder ein fehlendes Serverfeld. Nullable-Werte werden üblicherweise mit sicheren Aufrufen wie `user?.name`, dem Elvis-Operator wie `user?.name ?: "Guest"` oder expliziten Null-Prüfungen gehandhabt. Der `!!`-Operator umgeht die Sicherheitsprüfungen und wirft eine Ausnahme, falls der Wert `null` ist; er sollte daher nur selten und ausschließlich dann verwendet werden, wenn zweifelsfrei nachgewiesen werden kann, dass der Wert niemals `null` sein kann.
val displayName: String? = intent.getStringExtra("display_name")
val greeting = "Hello, ${displayName ?: "Guest"}"
val lengthText = displayName?.length?.toString() ?: "No name provided"
// Risky: crashes if displayName is null
val riskyLength = displayName!!.length
9Was ist der technische Unterschied zwischen der Übergabe eines Activity-Contexts und eines Application-Contexts an langlebige Objekte, und wie führt die falsche Wahl zu einem Speicherleck?
Ein Activity-Context ist direkt an den Lebenszyklus eines bestimmten Bildschirms und dessen UI-Hierarchie gebunden, während ein Application-Context an den Lebenszyklus des gesamten App-Prozesses gebunden ist. Wenn eine Activity beendet oder neu erstellt wird (z. B. bei Konfigurationsänderungen wie einer Bildschirmrotation), endet ihr Lebenszyklus und ihr Speicher soll vom GC (Garbage Collector) freigegeben werden. Übergibt man einen Activity-Context an ein langlebiges Objekt – wie ein Singleton, ein statisches Feld oder einen langlebigen Hintergrund-Thread –, behält dieses Objekt eine starke Referenz (Strong Reference) auf die Activity. Da langlebige Objekte als GC-Roots fungieren oder von GC-Roots aus erreichbar sind, kann der Garbage Collector die zerstörte Activity nicht bereinigen. Dadurch bleibt nicht nur die Activity-Instanz selbst im Speicher, sondern auch ihre gesamte angehängte View-Hierarchie, Drawables und Ressourcen, was zu einem erheblichen Speicherleck führt. Im Gegensatz dazu ist die Übergabe von `applicationContext` für langlebige Komponenten sicher, da dessen Lebensdauer der Prozesslaufzeit entspricht.
class LocationRepository private constructor(private val context: Context) {
companion object {
@Volatile
private var INSTANCE: LocationRepository? = null
fun getInstance(context: Context): LocationRepository {
return INSTANCE ?: synchronized(this) {
// SAFE: Using context.applicationContext prevents retaining an Activity reference
INSTANCE ?: LocationRepository(context.applicationContext).also { INSTANCE = it }
}
}
}
}
10Was ist der Unterschied zwischen einem Upload-Schlüssel und einem App-Signaturschlüssel bei der Verwendung von Google Play App Signing?
Bei Google Play App Signing erfüllen der Upload-Schlüssel und der App-Signaturschlüssel zwei unterschiedliche Sicherheitsaufgaben. Der Upload-Schlüssel verbleibt bei den Entwicklern, um das Build-Artefakt (AAB) vor dem Hochladen in die Google Play Console zu signieren und so die Identität des Entwicklers gegenüber Google zu verifizieren. Der App-Signaturschlüssel wird sicher in der Cloud-Infrastruktur von Google gespeichert und von Google Play verwendet, um die generierten APKs zu signieren, die tatsächlich an Endgeräte ausgeliefert und dort installiert werden; er begründet somit die dauerhafte kryptografische Identität der App unter Android. Ein wesentlicher betrieblicher Vorteil ist die Schlüsselwiederherstellung: Wenn Entwickler ihren Upload-Schlüssel verlieren oder dieser kompromittiert wird, kann der Google-Play-Support den Upload-Schlüssel nach Überprüfung der Entwickleridentität zurücksetzen, ohne den Update-Pfad der App zu unterbrechen. Im veralteten Modell, in dem Entwickler den App-Signaturschlüssel selbst verwalteten, führte der Verlust des Schlüssels dazu, dass die App nie wieder aktualisiert werden konnte.
11Ein Bildschirm verliert nach einer Gerätedrehung vom Benutzer eingegebene Formulardaten. Wie würden Sie systematisch diagnostizieren, ob das Problem an einem fehlenden gespeicherten Zustand (Saved State), einem falschen ViewModel-Scoping oder an der erneuten View-Bindung liegt?
Um systematisch zu diagnostizieren, warum Formulardaten nach einer Gerätedrehung verloren gehen, sollten drei Kernbereiche isoliert betrachtet werden:
1. **ViewModel-Scoping & Retention:** Überprüfen Sie, ob die ViewModel-Instanz über die Konfigurationsänderung hinweg tatsächlich beibehalten und nicht neu erstellt wird. Protokollieren Sie den Hash-Code des ViewModels (`viewModel.hashCode()`) über Drehungen hinweg. Stellen Sie sicher, dass es über Delegated Properties wie `by viewModels()` / `by activityViewModels()` oder `ViewModelProvider(this)` bezogen wird und nicht durch direkte Instanziierung über den Konstruktor (z. B. `MyViewModel()`).
2. **Saved State & Wiederherstellung von View-IDs:** Prüfen Sie, ob sich das Formular ausschließlich auf die standardmäßige Wiederherstellung der View-Hierarchie von Android verlässt. Views müssen ein im Layout definiertes `android:id`-Attribut besitzen, um an der automatischen Speicherung und Wiederherstellung des View-Zustands teilzunehmen. Bei benutzerdefinierten Views oder `SavedStateHandle` ist zu verifizieren, dass `onSaveInstanceState` / `SavedStateHandle` die relevanten Felder tatsächlich speichert und wiederherstellt.
3. **Erneute View-Bindung & Observer-Logik:** Untersuchen Sie `onViewCreated` oder Observer-Abonnements (`StateFlow`, `LiveData`). Prüfen Sie, ob neu erstellte Views Ereignisse wieder ordnungsgemäß abonnieren oder ob Initialisierungscode der View wiederhergestellten Text versehentlich mit leeren Standardwerten überschreibt. Prüfen Sie zudem auf Two-Way-Binding- oder `TextWatcher`-Schleifen, bei denen eine leere, wiederhergestellte View sofort ein leeres Update zurück in das ViewModel sendet.
class FormFragment : Fragment(R.layout.fragment_form) {
// 1. Ensure correct lifecycle scoping
private val viewModel: FormViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d("FormDiag", "VM instance hash: ${viewModel.hashCode()}")
// 2. Observe state without clobbering input during view recreation
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.formText.collect { text ->
if (binding.inputField.text.toString() != text) {
binding.inputField.setText(text)
}
}
}
}
}
}
12Wie ersetzt die moderne Jetpack Activity Result API das veraltete startActivityForResult und wie verhindert sie den Verlust von Callbacks bei der Neuerstellung einer Activity?
Die moderne Jetpack Activity Result API ersetzt das veraltete `startActivityForResult()` und `onActivityResult()` durch einen typsicheren, entkoppelten kontraktbasierten Mechanismus. Anstatt beliebige ganzzahlige Request-Codes und umfangreiche `onActivityResult`-Switch-Anweisungen zu verwalten, definieren Aufrufer einen `ActivityResultContract<I, O>` (der die Eingabe-Intent-Parameter und die erwartete geparste Ausgabe festlegt) und registrieren einen typisierten Callback über `registerForActivityResult()`, um einen `ActivityResultLauncher` zu erhalten. Die API schützt vor dem Verlust von Callbacks bei der Neuerstellung einer Activity oder einem Prozessabbruch durch eine strikte Lifecycle-Registrierung: 1. Wenn eine externe Activity (wie eine Kamera oder ein Document Picker) gestartet wird, kann die aufrufende Activity vom Betriebssystem aufgrund von Speicherdruck oder Konfigurationsänderungen zerstört werden. 2. Da `registerForActivityResult()` bedingungslos aufgerufen werden muss, bevor die Activity oder das Fragment den Zustand `STARTED` erreicht (typischerweise als Property-Initialisierer oder innerhalb von `onCreate`), wird der Ergebnis-Callback in der `ActivityResultRegistry` registriert, bevor die Zustandswiederherstellung stattfindet. 3. Wenn die gestartete Activity zurückkehrt und die Host-Activity neu erstellt wird, ordnet die `ActivityResultRegistry` das ausstehende Ergebnis des Systems dem neu registrierten Callback zu und übermittelt das Ergebnis sicher.
class ProfileActivity : AppCompatActivity() {
// Registered unconditionally during initialization / before onStart()
private val takePictureLauncher = registerForActivityResult(
ActivityResultContracts.TakePicturePreview()
) { bitmap: Bitmap? ->
bitmap?.let {
findViewById<ImageView>(R.id.avatarImage).setImageBitmap(it)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_profile)
findViewById<Button>(R.id.btnCapture).setOnClickListener {
takePictureLauncher.launch(null)
}
}
}
13Vergleichen Sie MVVM und MVI für einen komplexen Android-Bildschirm. Was macht Zustandsupdates atomar, und welche Wettlaufsituationen bleiben trotz unveränderlichem Zustand und Reducer?
Die Architekturnamen Model–View–ViewModel (MVVM) und Model–View–Intent (MVI) garantieren keine korrekte Nebenläufigkeit. MVVM kann einen unveränderlichen UiState mit Reducer verwenden, während eine MVI-Implementierung weiterhin Wettlaufsituationen enthalten kann. Ein Reducer berechnet aus Zustand und Ereignis den Folgezustand. Atomare Veröffentlichung erfordert etwa serialisierte Ereignisse, Mutex oder MutableStateFlow.update für einen einzelnen Wert. update kann bei Konkurrenz den Reducer mehrfach aufrufen; er muss frei von Seiteneffekten bleiben. Zusammengehörige Felder in einem Snapshot verhindern Teilupdates, aber keine fachlich ungültigen Zustände oder veraltete Netzantworten, die neuere überschreiben. Definieren Sie Invarianten, Generationen von Anfragen und Abbruch oder Ignorieren überholter Ergebnisse. Effekte gehören außerhalb des reinen Reducers. Explizite Intents und Übergänge erleichtern Nachverfolgung und Tests und sind auch in MVVM möglich. Testen Sie unterschiedliche zeitliche Verzahnungen von Ereignissen und Fehlerpfade statt sich auf das Label zu verlassen.
data class ScreenState(
val isLoading: Boolean = false,
val items: List<Item> = emptyList(),
val error: String? = null
)
sealed interface ScreenIntent {
data object Refresh : ScreenIntent
data class DataLoaded(val items: List<Item>) : ScreenIntent
data class LoadFailed(val message: String) : ScreenIntent
}
fun reduce(state: ScreenState, intent: ScreenIntent): ScreenState = when (intent) {
is ScreenIntent.Refresh -> state.copy(isLoading = true, error = null)
is ScreenIntent.DataLoaded -> state.copy(isLoading = false, items = intent.items, error = null)
is ScreenIntent.LoadFailed -> state.copy(isLoading = false, error = intent.message)
}
14Wie gestalten Sie Laufzeitberechtigungen mit Erstfrage, Begründung, Ablehnung, „Nicht mehr fragen“ und Einstellungen ohne Nutzerdruck?
Fragen Sie erst bei tatsächlichem Bedarf, etwa beim Tippen auf Codescan, nicht pauschal beim Start. Liefert shouldShowRequestPermissionRationale() den Wert true, erklären Sie in der App, beispielsweise in einem Bottom Sheet oder Dialog, warum die Berechtigung benötigt wird und welchen Nutzen sie bietet, bevor Sie den Systemdialog öffnen. Respektieren Sie Ablehnung, lassen Sie unabhängige Funktionen frei und bieten Sie etwa manuelle Eingabe statt Kamera. Der Wert false allein beweist keine dauerhafte Ablehnung: Es tritt auch vor der ersten Frage auf. Erfassen Sie, ob bereits eine Anfrage gestellt wurde, und berücksichtigen Sie deren Ergebnis und berücksichtigen Sie Plattform- oder Richtlinienbeschränkungen. Ist kein weiterer Dialog verfügbar, erklären Sie die fehlende Funktion und bieten Sie eine optionale Schaltfläche an, die über Settings.ACTION_APPLICATION_DETAILS_SETTINGS zu den App-Einstellungen führt. Nutzer müssen abbrechen oder zurückkehren können, ohne eingeschlossen zu werden.
fun onScanButtonClicked() {
when {
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
openScanner()
}
activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
showRationaleDialog(onConfirm = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) })
}
else -> {
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
}
fun handlePermissionResult(isGranted: Boolean) {
if (isGranted) {
openScanner()
} else if (!activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
showSettingsRedirectDialog()
} else {
showManualEntryFallback()
}
}
15Wie würden Sie eine Android-CI/CD-Pipeline (Continuous Integration / Continuous Delivery) von der Validierung von Pull Requests über die Erstellung signierter Release Candidates, interne Tests und den Upload in Google-Play-Tracks bis hin zum Rollout entwerfen?
Eine produktionsreife Android-CI/CD-Pipeline beginnt in der Validierungsphase von Pull Requests mit schnellen, automatisierten Prüfungen: statische Codeanalyse (ktlint, detekt, Android Lint), Unit-Tests und das Kompilieren eines Debug-Builds. Nach dem Merge in den Main- oder Release-Branch wird die Release-Pipeline ausgelöst, um ein signiertes Release Candidate (RC) Android App Bundle (`.aab`) zu erstellen. Dabei kommen in der CI hinterlegte Keystore-Zugangsdaten, Code-Shrinking via ProGuard/R8 und automatische Versionserhöhungen zum Einsatz. Das signierte AAB und die Mapping-Dateien zur Deobfuskierung werden mit Werkzeugen wie Gradle Play Publisher (GPP) oder Fastlane in den internen Test-Track von Google Play (oder Firebase App Distribution) hochgeladen, wo automatisierte Smoke- oder Instrumentierungstests laufen. Sobald die interne Qualitätssicherung und die Stakeholder den Build freigeben, wird exakt dasselbe Artefakt weitergereicht (Intern -> Closed Alpha/Beta -> Produktions-Rollout), ohne den Code erneut zu bauen. Schließlich sorgen gestaffelte Rollouts (z. B. 5 % -> 20 % -> 100 %) in Kombination mit automatisierter Überwachung von Absturzraten und Android Vitals für ein sicheres Deployment.
lane :promote_internal_to_beta do
upload_to_play_store(
track: 'internal',
track_promote_to: 'beta',
skip_upload_aab: true, # Promotes existing binary without rebuilding
skip_upload_metadata: false,
skip_upload_changelogs: false
)
end
16Warum führt das Abfangen von generischen `Exception`- oder `Throwable`-Instanzen ohne erneutes Auslösen von `CancellationException` zum Fehlschlagen des Coroutine-Abbruchs, und wie diagnostizieren Sie solche Abbruchprobleme?
Kotlin Coroutines basieren auf kooperativem Abbruch, der über `CancellationException` implementiert ist. Wenn das `Job`-Objekt einer Coroutine abgebrochen wird, werfen suspendierende Funktionen (wie `delay` oder `yield`) eine `CancellationException`, um den Callstack abzuwickeln und die Coroutine zu beenden. Fängt der Code generische `Exception`- oder `Throwable`-Instanzen ab, ohne `CancellationException` erneut auszulösen, wird das Abbruchsignal verschluckt. Die Coroutine bricht nicht ab und läuft weiter; es entsteht eine „Zombie-Coroutine“, die CPU und Speicher verschwendet, Ressourcen leckt und ungültige Zustandsübergänge oder Abstürze an zerstörten UI-Komponenten verursachen kann. Diagnose von Abbruchproblemen: 1. Fehlerbehandlung überprüfen und sicherstellen, dass `CancellationException` explizit erneut geworfen oder nur domänenspezifische Ausnahmen statt generischer `Throwable`/`Exception` abgefangen werden. 2. Coroutine-Zustände in Debug-Builds mit dem Kotlin Coroutines Debugger (`-Dkotlinx.coroutines.debug`) oder dem Android Studio Coroutines Inspector analysieren, um laufende Coroutines zu identifizieren, die eigentlich beendet sein sollten. 3. Lebenszykluszustände von Coroutines protokollieren (z. B. `job.isActive`, `job.isCancelled`) oder strukturiertes Logging in Abbruch-Completion-Handlern (`job.invokeOnCompletion`) einsetzen.
import kotlinx.coroutines.CancellationException
// ANTI-PATTERN: Swallows cancellation, creating a zombie coroutine
try {
doSuspendingWork()
} catch (e: Exception) {
logError(e)
}
// CORRECT: Preserves cancellation propagation
try {
doSuspendingWork()
} catch (e: Exception) {
if (e is CancellationException) throw e
logError(e)
}
17Wie funktionieren State Hoisting und UDF (Unidirectional Data Flow) in Jetpack Compose, und wie entscheiden Sie, ob ein Zustand in ein Leaf-Composable, ein übergeordnetes Composable oder ein ViewModel gehört?
State Hoisting ist ein Entwurfsmuster in Jetpack Compose, bei dem der Zustand in der Hierarchie der Komposition nach oben verschoben wird, um ein Composable zustandslos zu machen und in eine reine UI-Komponente zu verwandeln. Dies ermöglicht einen unidirektionalen Datenfluss (UDF, Unidirectional Data Flow), bei dem der Zustand nach unten fließt (vom übergeordneten Element/ViewModel als Parameter an untergeordnete Composables) und Ereignisse nach oben fließen (von untergeordneten Composables als Callbacks an übergeordnete Elemente/ViewModel).
Die Entscheidung über den Speicherort des Zustands folgt diesen Richtlinien:
1. **Leaf-Composable (lokaler UI-Zustand):** Ist der Zustand rein flüchtig, rein visuell und wird weder von übergeordneten noch von Geschwister-Komponenten benötigt (z. B. ob eine interne Ein-/Ausklapp-Animation läuft oder ein lokaler Ripple-Effekt aktiv ist), verbleibt er mittels `remember` lokal im Leaf-Composable.
2. **Übergeordnetes Composable (gehoisteter UI-Zustand):** Wenn Geschwister-Composables den Zustand teilen oder darauf reagieren müssen oder wenn das übergeordnete Element Sichtbarkeit bzw. Validierung steuert, wird der Zustand zum unmittelbar gemeinsamen Elternelement verschoben. Das Leaf-Composable wird zustandslos (erhält `value` und `onValueChange`).
3. **ViewModel (Screen-/Geschäftszustand):** Repräsentiert der Zustand Geschäftsdaten, überdauert Konfigurationsänderungen, steuert die Navigation oder erfordert Interaktionen mit Domain- bzw. Repository-Schichten, gehört er in ein ViewModel und wird als beobachtbarer Zustand offengelegt (z. B. via `StateFlow`). Das ViewModel verarbeitet die Geschäftslogik und aktualisiert den UI-Zustand entsprechend.
18Wie gestalten Sie Telemetrie, Ereignisspuren vor Abstürzen und Architekturregeln, um Zustandsverluste im FragmentManager und Wettlaufsituationen bei der Navigation im Produktivbetrieb zu erkennen, zu untersuchen und zu verhindern?
Eine Transaktion nach dem Speichern des FragmentManager-Zustands kann mit IllegalStateException abgewiesen werden, weil sie in der gespeicherten Momentaufnahme nicht enthalten wäre. Erfassen Sie von sensiblen Daten bereinigte Ereignisspuren zum Lebenszyklus von Host, Fragment und View, zu FragmentManager.isStateSaved, aktuellem Ziel, Task- und Instanzkennungen, Navigationsanforderungskennungen, Thread und Transaktionszeitpunkt. Führen Sie Navigation der Benutzeroberfläche im Hauptthread aus, verwenden Sie einen geeigneten Lebenszyklus-Owner und serialisieren Sie Anforderungen über eine zentrale Navigationszuständigkeit. Prüfen Sie vor dem Commit sowohl das aktuelle Ziel als auch, ob der Manager Zustandsänderungen annehmen kann. Verschieben oder verwerfen Sie veraltete Anforderungen nach einer ausdrücklichen Regel. Lebenszyklusgerechte Ereignisverarbeitung verhindert für sich allein weder doppelte Ereignisse noch Wettlaufsituationen zwischen Prüfung und Verwendung. Wenn zuverlässige Zustellung wichtig ist, sollte die Navigationsabsicht dauerhaft im Zustand abgebildet und ihre Verarbeitung bestätigt werden. Für einmalige Ereignisse gedachte Streams können je nach Konfiguration Ereignisse verlieren oder erneut ausliefern. Die tatsächliche Methode commitAllowingStateLoss() akzeptiert bewusst, dass eine Transaktion nach der Wiederherstellung fehlen kann; verwenden Sie sie nur für wirklich entbehrliche Zustände der Benutzeroberfläche. Jetpack Navigation und Fragment StrictMode bieten Struktur und ausgewählte Diagnosen, garantieren aber keine vollständige Freiheit von Zustandsverlusten oder Navigationsrennen. Testen Sie schnelle wiederholte Eingaben, verspätete Callbacks, Wechsel in den Hintergrund, Drehungen und Prozessneuerstellung.
class NavigationDispatcher @Inject constructor() {
private val _events = Channel<NavigationCommand>(Channel.BUFFERED)
val events: Flow<NavigationCommand> = _events.receiveAsFlow()
fun navigate(command: NavigationCommand) {
_events.trySend(command)
}
}
// In Fragment / Activity
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
navigationDispatcher.events.collect { command ->
CrashReporting.leaveBreadcrumb("Navigating to ${command.destination} at state ${lifecycle.currentState}")
command.execute(parentFragmentManager)
}
}
}
19Wie beeinflussen Doze, Standby-Buckets und herstellerspezifische Prozessbeendigung Hintergrundplanung, und wie bauen Sie eine robuste Synchronisierung mit WorkManager?
Doze beschränkt bei Inaktivität Prozessornutzung (CPU), Netzwerkzugriff und Hintergrundaufgaben und verschiebt viel gewöhnliche Arbeit in Wartungsfenster mit dokumentierten Ausnahmen. App-Standby-Buckets drosseln die Häufigkeit von Hintergrundjobs und den Netzwerkzugriff dynamisch danach, wie kürzlich die App genutzt wurde; die Einstufungen reichen von Active bis Restricted oder Never. Aggressive Energiesparmechanismen der Gerätehersteller (OEMs) wie MIUI oder OneUI können Prozesse beenden, Alarme ignorieren und Autostart einschränken, abweichend vom Verhalten des offenen Android-Basisprojekts (AOSP). WorkManager plant persistent im Rahmen der Plattform, umgeht aber weder Doze, Quoten, Force-stop noch Anbietergrenzen. Bedingungen wie NetworkType.CONNECTED und requiresBatteryNotLow verschieben Arbeit bis zu zulässiger Ausführung und Konnektivität. Gegen Prozessende und Netzfehler verwenden Sie geeignete Wiederholungen, häufig exponentielles Backoff, unter Berücksichtigung zufälliger Wartezeitstreuung und serverseitiger Ratenlimits; exponentielle Abstände allein verhindern keine synchronen Wellen. Worker benötigen atomare lokale Checkpoints und idempotente Fernoperationen, nicht die Annahme einer atomaren Transaktion über Gerät und Server. Deterministische Transaktions-IDs, lokale Zustandsflags und Serverdeduplizierung erlauben Wiederanlauf ohne doppelte Daten oder lokale Korruption.
20Wie gestalten Sie eine skalierbare Mehrteamarchitektur mit API-/Implementierungsmodulen für schnelle Gradlebuilds und strikte Grenzen?
Das Muster trennt pro Funktion oder Domäne zwei Gradle-Unterprojekte: ein leichtgewichtiges `:feature:api` mit öffentlichen Schnittstellen, Modellen und Navigationsverträgen sowie `:feature:impl` mit interner Geschäftslogik, Benutzeroberfläche und Repository-Implementierungen. Abhängige Module verwenden ausschließlich `:feature:api`, etwa mit `implementation project(':feature:api')`. Das Wurzelmodul `:app` oder andere zentrale Zusammenbaupunkte verbinden die konkreten Implementierungen durch Bereitstellung der Abhängigkeiten, beispielsweise mit Dagger oder Hilt. Stabile öffentliche Schnittstellen und kleinere Klassenpfade für die Kompilierung können erneute Kompilierungen verringern. Dieser Vorteil muss gegen zusätzlichen Modul- und Verknüpfungsaufwand gemessen werden. Ändern sich nur interne Details in `:feature:impl`, bleibt die öffentliche binäre Schnittstelle (ABI) von `:feature:api` unverändert. Abhängige Module mit unveränderten Kompilierungseingaben können dann ohne erneute Kompilierung auskommen. Betroffene Arbeiten an Implementierungsmodulen und der App-Paketierung bleiben erforderlich. Annotationsverarbeitung, erzeugter Code oder nach außen gelangte ABI-Typen können die Menge ungültig gewordener Ergebnisse vergrößern. Der Build Cache verwendet Aufgabenergebnisse wieder; der Configuration Cache verwendet den bereits konfigurierten Aufgabengraphen wieder. Das sind unterschiedliche Mechanismen. Organisatorisch schaffen etwa CODEOWNERS klare Teamzuständigkeiten. Zusammen mit Abhängigkeitsprüfungen können Teams interne Implementierungen weiterentwickeln, ohne private Klassen offenzulegen und versehentliche Kopplung zu erhöhen. Ohne Kontrolle des Abhängigkeitsgraphen können allerdings auch Schnittstellenmodule Zyklen bilden.