Every Android developer knows the Activity lifecycle diagram. The bugs come from the edge cases — the scenarios that aren't in the happy path.
Activity Lifecycle: Beyond the Basics
The standard callbacks in order: onCreate → onStart → onResume → [running] → onPause → onStop → onDestroy
But these edge cases trip developers up:
Configuration Changes (Rotation)
onPause → onStop → onSaveInstanceState → onDestroy
→ onCreate (savedInstanceState != null) → onStart → onResume
onSaveInstanceState is called before onDestroy. Use it to save UI state that ViewModel doesn't hold (scroll position, text input, selected tab).
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("scrollY", binding.scrollView.scrollY)
// ViewModel data is NOT saved here — ViewModel survives rotation
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
binding.scrollView.scrollY = savedInstanceState.getInt("scrollY")
}
Back Press & Predictive Back (Android 13+)
Legacy onBackPressed() is deprecated. Use OnBackPressedDispatcher:
onBackPressedDispatcher.addCallback(this) {
if (viewModel.hasUnsavedChanges()) {
showDiscardDialog()
} else {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
Predictive back (Android 13+) animates before the user commits to back — onBackPressedCallback receives a BackEvent during the gesture.
Multi-Window & PiP
When your app enters multi-window, onPause is called but NOT onStop (the app is still visible). Code that assumes "paused = not visible" breaks here.
// ❌ Wrong assumption
override fun onPause() {
videoPlayer.pause() // But user might still be watching in multi-window!
}
// ✅ Check visibility
override fun onPause() {
if (!isInMultiWindowMode) {
videoPlayer.pause()
}
}
Picture-in-Picture transitions: onUserLeaveHint() fires before PiP mode enters, giving you a chance to enter PiP:
override fun onUserLeaveHint() {
enterPictureInPictureMode(PictureInPictureParams.Builder().build())
}
Fragment Lifecycle: The Extra Complexity
Fragments have two separate lifecycle scopes:
- Fragment lifecycle (create → destroy): created with the Fragment, destroyed only when Fragment leaves the back stack
- View lifecycle (
viewLifecycleOwner): created inonCreateView, destroyed inonDestroyView
Fragment lifecycle: onCreate ────────────────────────── onDestroy
View lifecycle: onCreateView ─ onDestroyView
Critical bug: accessing views in the wrong scope:
// ❌ Observing with 'this' (fragment lifecycle)
// viewModel data delivered after onDestroyView → crash accessing binding
viewModel.data.observe(this) {
binding.textView.text = it.name // binding may be null!
}
// ✅ Observing with viewLifecycleOwner — stops when view is destroyed
viewModel.data.observe(viewLifecycleOwner) {
binding.textView.text = it.name // safe
}
ViewPager2 Offscreen Pages
ViewPager2 keeps offscreen fragments in STARTED state, not RESUMED. Code expecting RESUMED for business logic will break.
// ❌ Only called when fragment is the current page
override fun onResume() {
loadData() // skipped for offscreen pages!
}
// ✅ Use repeatOnLifecycle with STARTED
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { render(it) }
}
}
DialogFragment Lifecycle
DialogFragment creates its Dialog in onCreateDialog(). The dialog's views are NOT the Fragment's views — onCreateView is separate and optional.
class ConfirmDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(requireContext())
.setTitle("Confirm")
.setPositiveButton("Yes") { _, _ ->
// communicate result via shared ViewModel or Fragment Result API
setFragmentResult("confirm", bundleOf("ok" to true))
}
.create()
}
}
Common Lifecycle Bugs
| Bug | Root cause | Fix |
|---|---|---|
View access after onDestroyView | Observing with this instead of viewLifecycleOwner | Use viewLifecycleOwner |
| Listener leak | Registering in onStart but not unregistering in onStop | Mirror: register/unregister in the same scope |
| Rotation crash | Accessing ViewModel before onAttach completes | Always access VM after super.onCreate() |
| Multi-window video pause | Pausing on onPause always | Check isInMultiWindowMode |
| Fragment transaction crash | Committing after onSaveInstanceState | Use commitAllowingStateLoss() or delay |