Lesson 13 of 83 intermediate

Activity & Fragment Lifecycle, Configuration Changes

Master the lifecycle — the most-asked Android interview topic of all time

Open interactive version (quiz + challenge)

Real-world analogy

Activity lifecycle is like a restaurant table. onCreate is when guests sit down, onResume is when food arrives and they're actively eating, onPause is when they're distracted by a phone call, onStop is when they step outside, and onDestroy is when they leave and the table is cleared. Configuration change is like the restaurant switching to a new layout — every guest (Activity) is re-seated from scratch unless you have a VIP reservation (ViewModel).

What is it?

The Activity lifecycle defines how Android manages UI components through state transitions. Configuration changes (rotation, locale switch) destroy and recreate Activities — but ViewModels persist across this. onSaveInstanceState provides a Bundle-based escape hatch for small UI state that must survive process death. Fragment lifecycle adds a second dimension — the View lifecycle — that must be used correctly to avoid memory leaks and NPEs.

Real-world relevance

In a field operations app, a WorkOrderDetailActivity must restore scroll position and expanded section state after rotation — these go in onSaveInstanceState. The WorkOrder data itself lives in a ViewModel so it doesn't reload from the server on every rotation. A MapFragment for GPS tracking uses onStart/onStop to register/unregister location updates, ensuring battery drain only occurs when the map is visible. The Fragment uses viewLifecycleOwner for LiveData observation to prevent leaks when navigating to sub-screens.

Key points

Code example

// Activity with proper lifecycle handling
class WorkOrderDetailActivity : AppCompatActivity() {

    private val viewModel: WorkOrderViewModel by viewModels()
    private lateinit var binding: ActivityWorkOrderDetailBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityWorkOrderDetailBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Restore UI state (scroll position) — NOT data
        savedInstanceState?.let { state ->
            binding.scrollView.scrollTo(0, state.getInt(KEY_SCROLL_Y, 0))
            binding.detailsSection.isExpanded = state.getBoolean(KEY_DETAILS_EXPANDED, false)
        }

        // Data comes from ViewModel — survives config change
        viewModel.workOrder.observe(this) { order ->
            binding.titleText.text = order.title
            binding.statusChip.text = order.status.label
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        // Save transient UI state only
        outState.putInt(KEY_SCROLL_Y, binding.scrollView.scrollY)
        outState.putBoolean(KEY_DETAILS_EXPANDED, binding.detailsSection.isExpanded)
    }

    companion object {
        private const val KEY_SCROLL_Y = "scroll_y"
        private const val KEY_DETAILS_EXPANDED = "details_expanded"
    }
}

// Fragment with CORRECT lifecycle usage
class OrderMapFragment : Fragment(R.layout.fragment_order_map) {

    private val viewModel: WorkOrderViewModel by activityViewModels()
    private var _binding: FragmentOrderMapBinding? = null
    private val binding get() = _binding!!

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        _binding = FragmentOrderMapBinding.bind(view)

        // viewLifecycleOwner — not 'this' — to avoid leaks when on backstack
        viewModel.location.observe(viewLifecycleOwner) { loc ->
            binding.mapView.updateMarker(loc)
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null  // Prevent memory leak — view is gone but fragment lives
    }
}

Line-by-line walkthrough

  1. 1. WorkOrderDetailActivity extends AppCompatActivity and uses by viewModels() delegation — this hooks into ViewModelProvider under the hood and returns the same ViewModel instance across configuration changes.
  2. 2. savedInstanceState?.let { } in onCreate only runs if this is a recreation — not first launch. It restores scroll position and section expansion — small UI state that ViewModel doesn't own.
  3. 3. viewModel.workOrder.observe(this) uses 'this' (the Activity) as LifecycleOwner — correct for Activities because Activity lifecycle and view lifecycle are the same.
  4. 4. onSaveInstanceState saves scroll Y and details expansion state. putInt/putBoolean are efficient — this Bundle is stored in system memory and limited to ~1MB total.
  5. 5. OrderMapFragment uses by activityViewModels() to share the ViewModel with the Activity — both see the same WorkOrder and location data.
  6. 6. _binding is nullable and set to null in onDestroyView — this is the standard pattern. The backing property 'binding' asserts non-null via !! and will crash if accessed after onDestroyView, which is the desired behavior (fail fast).
  7. 7. viewModel.location.observe(viewLifecycleOwner) is the critical line — viewLifecycleOwner is tied to the Fragment's VIEW lifecycle, not the Fragment lifecycle. When the Fragment goes to the backstack, the view is destroyed, this observer is removed, preventing memory leaks.
  8. 8. onDestroyView sets _binding = null — without this, the binding holds a reference to the view hierarchy, and since the Fragment (which is still alive on the backstack) holds binding, the views can never be garbage collected — a classic memory leak.

Spot the bug

class ProfileFragment : Fragment() {
    private val binding: FragmentProfileBinding by lazy {
        FragmentProfileBinding.inflate(layoutInflater)
    }
    private val viewModel: ProfileViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View = binding.root

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.profile.observe(this) { profile ->  // bug 1
            binding.nameText.text = profile.name
            binding.emailText.text = profile.email
        }
    }

    // No onDestroyView override
}

class SettingsActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)
        loadHeavyData()  // bug 2
    }

    override fun onPause() {
        super.onPause()
        saveAllSettings()    // bug 3
        uploadSettingsToServer()
    }
}
Need a hint?
Think about Fragment memory leaks with lazy binding, LiveData observer lifecycle, and heavy operations in lifecycle callbacks.
Show answer
Bug 1: observe(this) uses Fragment lifecycle — when the Fragment goes to backstack, its view is destroyed but Fragment survives. The observer still fires and accesses binding (which references destroyed views), risking NPE. Fix: observe(viewLifecycleOwner). Bug 2 (minor): loadHeavyData() in onCreate is fine for initialization, but if it blocks the main thread it causes jank. Better: call it and observe a ViewModel LiveData that does async loading. Bug 3: uploadSettingsToServer() in onPause is dangerous — onPause must return fast because the next Activity waits for it. Network call here causes visible lag on the incoming screen. Fix: enqueue a WorkManager task for reliable background upload, or at minimum move to a coroutine with lifecycleScope.launch.

Explain like I'm 5

Think of an Activity like a TV show playing on screen. onCreate is the show starting, onResume is you actively watching, onPause is someone calling you (show still visible but you're distracted), onStop is you leaving the room, onDestroy is turning the TV off. Rotating the device is like switching to a different TV — the show has to restart, but your remote control (ViewModel) remembers where you were. onSaveInstanceState is like writing a sticky note with where you paused so the new TV can jump to the right spot.

Fun fact

Android's configuration change system was designed in 2008 when devices had fixed orientations. The destroy-recreate cycle was meant to load landscape layouts automatically. Today, ViewModels and Jetpack Compose largely sidestep this problem — but every Android interview still tests it because legacy codebases are everywhere.

Hands-on challenge

Build a NoteEditorActivity that: (1) saves scroll position and cursor position in onSaveInstanceState, (2) restores them in onRestoreInstanceState, (3) keeps the Note object (with title, body, lastModified) in a ViewModel, (4) uses a NoteEditorFragment with viewLifecycleOwner for all LiveData observers, and (5) sets _binding = null in onDestroyView. Verify rotation doesn't lose the note content OR the UI state.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery