androidengineers.Book a session

Core Android Components Deep Dive

Activity & Fragment Lifecycles: Edge Cases

article30 minHard

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 in onCreateView, destroyed in onDestroyView
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

BugRoot causeFix
View access after onDestroyViewObserving with this instead of viewLifecycleOwnerUse viewLifecycleOwner
Listener leakRegistering in onStart but not unregistering in onStopMirror: register/unregister in the same scope
Rotation crashAccessing ViewModel before onAttach completesAlways access VM after super.onCreate()
Multi-window video pausePausing on onPause alwaysCheck isInMultiWindowMode
Fragment transaction crashCommitting after onSaveInstanceStateUse commitAllowingStateLoss() or delay

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Activity & Fragment Lifecycles: Edge Cases | Android System Design | Android Engineers