Java, LiveData und Fragment-Migration
Du lernst, wie du Java-, LiveData- und Fragment-Code schrittweise modernisierst. Fokus: Verhalten sichern und Releases stabil halten.
Viele Android-Apps bestehen nicht nur aus modernem Kotlin- und Compose-Code. In echten Projekten findest du oft Java-Klassen, LiveData im ViewModel und fragmentbasierte Screens. Migration heißt dann nicht, alles neu zu schreiben, sondern vorhandenes Verhalten kontrolliert in eine wartbare Richtung zu bewegen.
Was ist das?
Java-, LiveData- und Fragment-Migration beschreibt die schrittweise Modernisierung älterer Android-Architektur. Java-Interop bedeutet, dass Kotlin und Java im selben Projekt zusammenarbeiten. Du kannst also neue Klassen in Kotlin schreiben, während bestehender Java-Code weiter aufgerufen wird.
LiveData ist ein Lifecycle-bewusster Datenhalter aus den Architecture Components. Er passt zu ViewModels und Fragmenten, weil Beobachter automatisch an den Lifecycle gebunden werden können. In neueren Codebasen wird häufig Flow oder StateFlow genutzt, aber LiveData bleibt in vielen Apps relevant.
Fragmente sind UI-Bausteine, die einen eigenen Lifecycle haben und oft innerhalb einer Activity navigieren. Auch wenn Compose neue UI-Strukturen ermöglicht, verschwinden Fragmente in bestehenden Apps selten sofort. Sie enthalten Navigation, ViewBinding, Adapter, Dialoge oder alte XML-Layouts.
Das mentale Modell ist wichtig: Migration ist Refactoring unter Produktdruck. Du willst die innere Struktur verbessern, ohne Nutzerfunktionen, Analytics, Deep Links, Back Navigation oder Release-Stabilität zu beschädigen.
Wie funktioniert es?
Eine gute Migration beginnt mit Grenzen. Du suchst zuerst stabile Schnittstellen: ViewModel, Repository, Navigator, Use Case oder UI-State. Danach ersetzt du einzelne Teile, nicht den ganzen Screen. So bleibt der Vergleich zwischen altem und neuem Verhalten möglich.
Bei Java-Interop musst du auf Nullbarkeit achten. Kotlin unterscheidet nullable und non-null Typen, Java aber nicht zuverlässig. Dadurch entstehen Plattformtypen. Ein Java-Getter kann in Kotlin wie ein sicherer Wert aussehen, obwohl zur Laufzeit null kommt. Deshalb prüfst du Java-APIs an den Übergängen besonders genau.
Bei LiveData ist der Lifecycle entscheidend. In Fragmenten beobachtest du UI-Daten mit viewLifecycleOwner, nicht mit dem Fragment selbst. Sonst kann ein Observer länger leben als die View. Das führt zu doppelten Updates, Speicherproblemen oder Crashes nach einer Navigation.
Bei Fragmenten trennst du UI-Bindung, ViewModel-Zugriff und Navigation sauber. Das Fragment sollte möglichst wenig Fachlogik enthalten. Es sammelt Eingaben, rendert Zustand und leitet Events weiter. Produktlogik gehört ins ViewModel oder in darunterliegende Schichten.
In der Tagesarbeit sieht das oft unspektakulär aus: Du wandelst eine Java-Datenklasse in Kotlin um, ersetzt direkte Repository-Aufrufe im Fragment durch ein ViewModel oder führst einen UI-State ein. Jede Änderung sollte klein genug sein, um sie im Code-Review zu verstehen und mit Tests oder manuellen Checks abzusichern.
In der Praxis
Angenommen, ein alter Fragment-Screen lädt Profildaten über ein Java-Repository und zeigt sie per LiveData an. Eine sichere Zwischenstufe ist nicht sofort Compose, sondern zuerst eine klare Kotlin-Grenze im ViewModel.
class ProfileViewModel(
private val repository: ProfileRepository
) : ViewModel() {
private val _state = MutableLiveData<ProfileState>()
val state: LiveData<ProfileState> = _state
fun loadProfile(userId: String) {
val profile = repository.loadProfile(userId)
if (profile == null) {
_state.value = ProfileState.Error
} else {
_state.value = ProfileState.Content(
name = profile.name.orEmpty(),
email = profile.email.orEmpty()
)
}
}
}
class ProfileFragment : Fragment(R.layout.profile_fragment) {
private val viewModel: ProfileViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.state.observe(viewLifecycleOwner) { state ->
render(state)
}
viewModel.loadProfile(requireArguments().getString("userId").orEmpty())
}
private fun render(state: ProfileState) {
// UI aktualisieren: TextViews, Ladezustand, Fehlermeldung
}
}
Die wichtige Regel lautet: Ändere erst die Struktur, dann die UI-Technologie. Wenn du gleichzeitig Java nach Kotlin, LiveData nach Flow, Fragmente nach Compose und Navigation umbaust, findest du Fehler schwer wieder. Kleine Schritte machen Regressionen sichtbar.
Eine typische Stolperfalle ist der falsche Observer-Lifecycle. observe(this) im Fragment wirkt harmlos, bindet aber an den Fragment-Lifecycle statt an den View-Lifecycle. Wird die View zerstört und neu erstellt, kann alter UI-Zugriff weiterlaufen. Verwende bei View-Zugriffen im Fragment fast immer viewLifecycleOwner.
Eine zweite Stolperfalle ist verdeckte Logik im Fragment. Wenn beim Migrieren plötzlich Bedingungen verschwinden, etwa für leere Listen, Offline-Zustände oder Feature Flags, hast du kein reines Refactoring mehr. Schreibe vor der Änderung mindestens einfache Tests für ViewModel-Zustände oder dokumentiere die erwarteten Fälle im Pull Request.
Als Entscheidungsregel kannst du dir merken: Migriere zuerst Code, der eine klare Grenze hat und häufig geändert wird. Lasse riskante Release-Pfade, Zahlungsfunktionen oder Login-Flows nicht ohne Tests umbauen. Vor einem Release prüfst du besonders Startverhalten, Navigation zurück, Konfigurationswechsel und Fehlerzustände.
Fazit
Java-, LiveData- und Fragment-Migration ist solide Handwerksarbeit: Du modernisierst eine bestehende App in kleinen Schritten, schützt das sichtbare Verhalten und verbesserst dabei die Architektur. Prüfe dein Verständnis, indem du einen alten Fragment-Screen auswählst, seine Zuständigkeiten notierst, einen ViewModel-Test für den wichtigsten Zustand schreibst und im Debugger beobachtest, wann Observer registriert und wieder entfernt werden.