Activity Lifecycle
An Activity is never simply "on" or "off" — the Android system moves it through a well-defined
sequence of states as the user opens it, switches away, rotates the device, or returns to it. This
sequence is the Activity lifecycle, and every screen in TrailLog — the trail list,
TrailDetailActivity, a future settings screen — passes through the exact same states.
Knowing what to do (and what not to do) in each one is one of the first real Android
skills worth mastering.
The Full Lifecycle Sequence
The system calls onCreate() exactly once when the Activity is first built, then
onStart() and onResume() as it becomes visible and interactive. When the
user navigates away, the same journey happens in reverse: onPause(),
onStop(), and eventually onDestroy(). An Activity can also bounce back
from onStop() straight to onStart()/onResume() if the user
returns to it before it's destroyed — the sequence isn't always a straight line to the end.
What Belongs in Each Callback
| Callback | What goes here |
|---|---|
onCreate() | One-time setup: inflating the layout, binding views, reading Intent extras, setting up a ViewModel reference. |
onStart() | The Activity is about to become visible. Register anything the user should see reflected immediately. |
onResume() | The Activity is now in the foreground and interactive. Start anything that only makes sense while actively on screen — like a live location listener. |
onPause() | Another screen is partially covering this one. Stop anything you started in onResume() and save lightweight state. |
onStop() | The Activity is no longer visible at all. Release heavier resources you no longer need. |
onDestroy() | Final cleanup before the Activity object is discarded, whether the user is done with it or the system needs the memory back. |
Worked Example: Starting and Stopping a Location Listener
TrailDetailActivity shows a trail's saved latitude and longitude, but say it also
offers a "live distance from here" reading while the screen is open. It only makes sense to
listen for location updates while the user can actually see that number, so the listener starts
in onResume() and stops in onPause():
class TrailDetailActivity : AppCompatActivity() {
private var isListeningForLocation = false
override fun onResume() {
super.onResume()
startLocationUpdates()
isListeningForLocation = true
}
override fun onPause() {
super.onPause()
if (isListeningForLocation) {
stopLocationUpdates()
isListeningForLocation = false
}
}
}
If you started the listener in onCreate() instead and never stopped it, it would
keep draining the device's battery long after the user left the screen — a classic Android
resource leak that the pause/resume pairing exists specifically to prevent.
Surviving Configuration Changes
Rotating the device (or a handful of other configuration changes) destroys and recreates the
current Activity by default — a fresh onCreate() runs as if the screen had just been
opened. Any plain variable holding in-progress state, like text the user was mid-way through
typing into a trail's notes field, is lost unless you explicitly save it. This is a genuinely
common trap for people new to Android: the app "loses" what the user just typed the instant they
rotate the phone.
onSaveInstanceState() is called before the Activity is destroyed for this reason, and
onCreate()/onRestoreInstanceState() receive that saved Bundle
back afterward:
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("draft_notes", notesEditText.text.toString())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_trail_detail)
val draftNotes = savedInstanceState?.getString("draft_notes")
if (draftNotes != null) {
notesEditText.setText(draftNotes)
}
}
This is only meant for small, lightweight bits of transient UI state — not for storing whole
objects like a full Trail list. Persistent data belongs in Room or DataStore, both
covered later in this track.
The Modern Alternative: Lifecycle-Aware Components
Overriding six callbacks by hand works, but it scatters lifecycle-dependent logic across the
entire Activity class and makes it hard to reuse. Jetpack's Lifecycle library offers
a cleaner alternative: implement DefaultLifecycleObserver in a small, focused class
and attach it to the Activity's lifecycle instead of overriding methods on the Activity itself.
class LocationListenerObserver(private val onLocationTick: () -> Unit) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
// start listening for location updates
}
override fun onPause(owner: LifecycleOwner) {
// stop listening for location updates
}
}
You then register it once — lifecycle.addObserver(LocationListenerObserver { ... })
— and the object automatically receives the right calls at the right time, without the Activity
needing to override anything itself. This same lifecycle-awareness is what makes
LiveData able to stop delivering updates to a destroyed screen automatically, which
you'll see in ViewModel & LiveData.
onCreate(), and release anything that costs battery or memory — listeners, streams, connections — no later than onStop(). Anything left running past that point keeps working even though the user can no longer see it.