← Back to all posts

07 Xpense

Activities and the Activity Lifecycle

Xpense Dev Retrospective


What an Activity Actually Is

Every screen in Xpense, MainActivity, AddExpenseActivity, HistoryActivity, all nine of them, is a class that extends BaseActivity, which extends Android's own AppCompatActivity. An Activity isn't just "a screen." It's an object Android creates, manages, pauses, and eventually destroys on its own schedule, not on the app's schedule.


The Lifecycle, in the Order It Actually Fires

MethodWhen it fires
onCreate()Once, when the Activity is first created. Layout is set here, views are found here
onStart()Right before it becomes visible
onResume()Right before the user can actually interact with it
onPause()Right as it stops being the foreground screen
onStop()Once it's no longer visible at all
onDestroy()When it's being fully removed from memory

An Activity is less like a light switch, on or off, and more like an employee clocking in and out of different states throughout a shift. Android decides when those state changes happen. The app just gets notified and reacts.


Why MainActivity Needs onResume, Specifically

This is the clearest real example in the whole app. When someone adds an expense from AddExpenseActivity and taps back, MainActivity is still sitting in memory exactly as it was before, budget numbers and all. If the dashboard only refreshed in onCreate, it would show stale numbers, since onCreate doesn't run again just because the user came back to an existing screen.

MainActivity.kt
override fun onResume() {
    super.onResume()
    updateDashboard()
}

private fun updateDashboard() {
    lifecycleScope.launch {
        val spent = expenseRepo.getTotalSpentThisPeriod()
        // ...refresh every TextView on the dashboard with current data
    }
}

That's the entire fix for "why doesn't the dashboard update after I add an expense." Every time MainActivity becomes visible again, whether from a fresh launch or from backing out of another screen, onResume fires and pulls fresh numbers.


Why onCreate Reads the Intent Only Once

AddExpenseActivity checks for an edit_id extra inside onCreate, not inside onResume, because that check is about how this particular instance of the screen was launched, add mode or edit mode, and that answer never changes for the lifetime of this Activity instance. Putting it in onResume would just be redundant work run every time the screen becomes visible again for no reason.


onCreate Can Exit Before It Finishes

MainActivity.onCreate checks onboarding status before it wires up a single view:

MainActivity.kt
if (!settings.isOnboardingComplete()) {
    startActivity(Intent(this, OnboardingActivity::class.java))
    finish()
    return
}

Why finish() matters here: without it, backing out of OnboardingActivity would drop the user straight back onto a half-initialized MainActivity that already decided it shouldn't be shown. finish() removes this Activity instance from the back stack entirely, so there's nothing to accidentally return to. The return right after stops onCreate from continuing on to look up views that this code path never needs.


Multi-Step Screens Live Inside One Activity

OnboardingActivity has three logical steps, budget, cycle, week start, but it's a single Activity the whole time, not three. It tracks currentStep as a plain field and re-renders the content for that step in place:

OnboardingActivity.kt
private fun goNext() {
    when (currentStep) {
        Step.BUDGET -> { /* validate, then */ currentStep = Step.CYCLE; renderStep() }
        Step.CYCLE  -> { /* branch to WEEK_START or finish */ }
        Step.WEEK_START -> finishOnboarding()
    }
}

Why not three separate Activities: three Activities means three lifecycles to manage, three back-stack entries, and passing partial answers between them via Intent extras at every step. A single Activity swapping its own content avoids all of that. The lifecycle only has to be reasoned about once, not three times over.


Closing

The lifecycle is why onResume refreshes data, why finish() matters after onboarding, and why some screens stay single Activities instead of splitting apart. Next up, the other half of this: how Kotlin code actually reaches into a layout and wires up what happens when a button gets tapped.

← Previous 06 - Layouts and XML UI Next → 08 - Kotlin and the UI