Skip to main content

Fragments and the separate view lifecycle

The one distinction you must remember​

A Fragment has:

  1. a Fragment object lifecycle;
  2. a Fragment view lifecycle for the UI returned by onCreateView().

The view can be destroyed while the Fragment object stays alive on the back stack.

Fragment object: onCreate ───────────────────────────── onDestroy
View: onCreateView ── onDestroyView
Later new view: onCreateView ── onDestroyView

The official Fragment lifecycle documentation explicitly provides viewLifecycleOwner for work that should exist only while the Fragment’s view exists.

Why binding leaks​

❌ Retain the old view tree​

class ProfileFragment : Fragment(R.layout.profile) {
private lateinit var binding: ProfileBinding

override fun onViewCreated(view: View, state: Bundle?) {
binding = ProfileBinding.bind(view)
}
}

When navigation destroys the view but retains the Fragment, binding still references every View and its Activity context.

✅ Restrict binding to view lifetime​

class ProfileFragment : Fragment(R.layout.profile) {
private var _binding: ProfileBinding? = null
private val binding: ProfileBinding
get() = requireNotNull(_binding) {
"Binding used outside view lifecycle"
}

override fun onViewCreated(view: View, state: Bundle?) {
super.onViewCreated(view, state)
_binding = ProfileBinding.bind(view)
binding.retry.setOnClickListener { viewModel.retry() }
}

override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}

The non-null accessor is valid only between view creation and destruction. Background callbacks must not use it afterward.

Collect state with viewLifecycleOwner​

❌ Fragment lifecycle collection​

lifecycleScope.launch {
viewModel.uiState.collect { state -> render(binding, state) }
}

The Fragment may still be alive when its binding is gone.

✅ View lifecycle and repeatOnLifecycle​

viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state -> render(binding, state) }
}
}

Step by step:

  1. Collection launches in the view owner’s scope.
  2. At STARTED, a child collector begins.
  3. Below STARTED, that child is cancelled.
  4. If the same view restarts, collection restarts.
  5. When the view is destroyed, its scope is cancelled permanently.
  6. A future view gets a new owner and collector.

launchWhenStarted versus repeatOnLifecycle​

Older patterns can suspend a coroutine while upstream work remains active. repeatOnLifecycle cancels and relaunches the block as state changes, making resource ownership explicit. For multiple flows, collect concurrently inside the repeat block when each must progress independently.

repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { viewModel.uiState.collect(::render) }
launch { viewModel.effects.collect(::handleEffect) }
}

Arguments and recreation​

❌ Constructor argument​

class OrderFragment(private val orderId: String) : Fragment()

Android/FragmentManager must recreate the Fragment using its expected constructor/factory mechanism.

✅ Arguments/navigation route​

class OrderFragment : Fragment(R.layout.order) {
private val orderId: String by lazy {
requireArguments().getString("orderId")
?: error("Missing orderId")
}
}

Prefer typed Navigation arguments where available. Pass stable IDs, not large mutable domain graphs.

FragmentManager owns transactions​

Committing after state has been saved can produce IllegalStateException because Android cannot safely include the transaction in restored state.

❌ Hide with state loss​

transaction.commitAllowingStateLoss()

This suppresses the exception by accepting that navigation/UI state may disappear. It is not a general fix.

✅ Fix event/lifecycle ownership​

Deliver navigation only while the owner can act, model consumed events, and use Navigation/FragmentManager APIs at the correct lifecycle point. Use state loss only when losing that transaction is explicitly acceptable.

ViewPager2 and maximum lifecycle​

Fragments near the selected page can exist while only the current page is RESUMED. Do not infer “Fragment exists” means “user sees/interacts with it.” Use lifecycle state and visibility contracts.

Child fragments​

Use childFragmentManager for Fragments nested inside another Fragment’s UI. Using the Activity manager accidentally changes ownership and restoration hierarchy.

Fragment versus Compose​

Compose can reduce the need for many UI Fragments, but Fragments remain relevant for existing navigation, View interoperability, ViewPager2, modular boundaries, dialogs, and gradual migration. The same principle remains: UI collection/effects must match the lifetime of the actual rendered UI.

Interview-ready answer

A Fragment object can outlive its view, especially on the back stack. Therefore bindings, adapters, listeners, and UI collection belong to the view lifecycle, are cleared in onDestroyView, and use viewLifecycleOwner.repeatOnLifecycle. Persistent screen data belongs in a ViewModel/repository, while Fragment arguments contain small reconstructable inputs. I avoid commitAllowingStateLoss unless losing the UI transaction is an explicit requirement.

Exercises​

  1. Draw the Fragment and view lifecycles during replace/add-to-back-stack/pop.
  2. Why can a Fragment-scoped coroutine access a dead binding?
  3. Diagnose navigation that crashes only after app backgrounding.