What carried over directly

Null-safety concepts, general object-oriented structure, and reasoning about immutability all transferred almost unchanged from years of Java. Kotlin's stricter null handling felt like a formalization of habits I already had, not a new one to learn.

What didn't: the lifecycle

A backend service starts, runs, and shuts down on your terms. An Android activity or composable can be destroyed and recreated mid-task. A rotation or a low-memory event can wipe UI state you didn't explicitly save, in a way nothing in backend Java prepares you for.

@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }
    Column {
        Text("$count")
        Button(onClick = { count++ }) { Text("Increment") }
    }
}

Gradle felt familiar, then didn't

Kotlin DSL build scripts read close enough to the Groovy ones I already knew from backend Gradle projects, but the Android-specific pieces, build variants, KSP for annotation processing, version catalogs shared across modules, took longer to click than the language itself.

Coroutines as the answer to "don't block the main thread"

viewModelScope.launch felt closer to the structured-concurrency model from Kotlin coroutines than anything from traditional Java threading. The scope is tied to the screen's lifecycle, so work cancels automatically when the user navigates away.

Testing composables needed new tools

A JUnit test can't render a UI, so testing a composable means pulling in a different toolchain entirely: createComposeRule, semantic node matchers, and tests that behave more like a lightweight UI framework than the assertion-heavy unit tests I was used to writing for backend services.

Permissions are a runtime conversation, not a config file

A backend service either has access to a resource or it doesn't, decided once at deploy time. On Android, a permission like camera or location has to be requested from the user at runtime, can be denied, and has to be handled gracefully when it is, every single time the feature is used, not just the first. Designing the UI around a "permission denied" path that a user might hit on every launch was a category of state I'd never had to design for in backend work.

ProGuard/R8 found bugs that only existed in release builds

A debug build worked perfectly; the release build crashed on a reflection call that R8 had stripped as "unused" because nothing referenced it directly by name. Adding keep rules for the affected classes fixed it, but it meant a class of bug that a Java backend's identical build artifact for every environment simply doesn't have.

🎯 Takeaway
Compared with the SwiftUI project, the declarative UI model felt familiar the second time around. The real relearning on both platforms was the lifecycle and tooling, not the syntax.