Xpense Dev Retrospective
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.
| Method | When 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.
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.
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.
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.
MainActivity.onCreate checks onboarding status before it wires up a single view:
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.
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:
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.
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.