This is Day 4 of my 100 Days to Senior Android Engineer series. Each post: what I thought I knew → what I actually learned → interview implications.
🔍 The concept
If you thought Activity Lifecycle was tricky, Fragment Lifecycle is trickier — because a Fragment doesn't have one lifecycle. It has two.
- The Fragment lifecycle — tied to when the Fragment is attached to its host
- The Fragment View lifecycle — tied to when the Fragment's UI exists
They run in parallel, they start and end at different times, and confusing one for the other is responsible for a surprising number of memory leaks and NullPointerException crashes in production Android apps.
💡 What I thought I knew
I used to think Fragment Lifecycle was basically Activity Lifecycle with extra steps. onCreate, onStart, onResume, onPause, onStop, onDestroy — with a few Fragment-specific callbacks like onAttach and onCreateView sprinkled in.
That model gets you through most work. Until it doesn't.
😳 What I actually learned
The full Fragment lifecycle has 9 callbacks, not 6
onAttach() ← Fragment attached to Activity
onCreate() ← Fragment created (no view yet)
onCreateView() ← View inflated
onViewCreated() ← View ready to use ✅ best place for view setup
onStart()
onResume()
onPause()
onStop()
onDestroyView() ← View destroyed (Fragment still alive!)
onDestroy()
onDetach() ← Fragment detached from Activity
The critical pair that most tutorials gloss over: onCreateView() / onDestroyView().
The Fragment itself can be alive while its View is destroyed. This happens every time you navigate away from a Fragment in a back stack — the Fragment instance is retained, but its View is torn down and rebuilt when you navigate back.
The two lifecycles visualized
Fragment added to back stack, user navigates away:
Fragment lifecycle: CREATED ──────────────────────────────── CREATED
│ │
View lifecycle: CREATED ── STARTED ── RESUMED ── PAUSED ── STOPPED ── DESTROYED
↑
View gone, Fragment alive
When the user navigates back:
Fragment lifecycle: CREATED (same instance, uninterrupted)
│
View lifecycle: CREATED (brand new View) ── STARTED ── RESUMED
Same Fragment, new View. Every time.
The leak that this causes
Here's a pattern I've seen in real codebases — it looks completely reasonable:
class UserProfileFragment : Fragment() {
// 💀 Storing View binding at Fragment scope
private var binding: FragmentUserProfileBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentUserProfileBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onDestroyView() {
super.onDestroyView()
// Most tutorials tell you to null this out here
binding = null
}
}
Setting binding = null in onDestroyView() looks correct. And it is necessary. But it's not sufficient if you have observers or coroutines that still hold a reference to the old binding after the view is destroyed.
The safer pattern with coroutines:
class UserProfileFragment : Fragment(R.layout.fragment_user_profile) {
// Use viewBinding delegate — auto-nulled on onDestroyView
private val binding by viewBinding(FragmentUserProfileBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ✅ viewLifecycleOwner — scoped to the VIEW lifecycle, not the Fragment
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState.collect { state ->
binding.profileName.text = state.name
}
}
}
}
The key is viewLifecycleOwner — not this (the Fragment), not viewLifecycleOwner.lifecycle, but viewLifecycleOwner as the LifecycleOwner passed to observers and coroutine scopes.
this vs viewLifecycleOwner — the mistake that causes crashes
This is the most common Fragment lifecycle bug I've seen in code reviews:
// ❌ Wrong — uses Fragment as LifecycleOwner
viewModel.uiState.observe(this) { state ->
binding.profileName.text = state.name // 💀 binding might be null here
}
// ✅ Correct — uses View lifecycle as LifecycleOwner
viewModel.uiState.observe(viewLifecycleOwner) { state ->
binding.profileName.text = state.name // safe — observer cancelled when view dies
}
When you pass this (the Fragment) as the LifecycleOwner, the observer stays active even after onDestroyView(). The Fragment is still STARTED. So when uiState emits a new value while the View doesn't exist, your observer fires and tries to update binding — which is null. Crash.
When you pass viewLifecycleOwner, the observer is automatically cancelled when the View is destroyed. No null check needed. No crash.
The callback where I always initialize views wrong
onCreateView() vs onViewCreated() — which one do you use to set up click listeners and observers?
// ❌ Common but suboptimal
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentUserProfileBinding.inflate(inflater, container, false)
// Setting up UI logic here — works, but mixes concerns
binding.button.setOnClickListener { /* ... */ }
return binding.root
}
// ✅ Correct separation
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// ONLY inflate and return — nothing else
_binding = FragmentUserProfileBinding.inflate(inflater, container, false)
return _binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ALL view setup goes here — binding is guaranteed non-null
binding.button.setOnClickListener { /* ... */ }
observeViewModel()
}
onViewCreated() is called immediately after onCreateView() with the view already inflated and attached. It's the right place for view setup because the view is guaranteed to exist, and using viewLifecycleOwner here is unambiguous.
Fragment-to-Fragment communication the right way
One more pattern that trips up mid-to-senior level devs: how Fragments should talk to each other.
// ❌ Fragment directly referencing sibling Fragment
class DetailFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// Getting sibling Fragment — tightly coupled, breaks on backstack changes
val listFragment = parentFragmentManager
.findFragmentById(R.id.list_fragment) as? ListFragment
listFragment?.updateSelection(itemId) // 💀 might be null
}
}
// ✅ Communicate through shared ViewModel
class DetailFragment : Fragment() {
// Scoped to Activity — shared with ListFragment
private val sharedViewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.button.setOnClickListener {
sharedViewModel.selectItem(itemId) // ListFragment observes this
}
}
}
Fragments should never hold direct references to sibling Fragments. The ViewModel is the bus.
🧪 The mental model that stuck
Two rules that cover 90% of Fragment lifecycle issues:
Rule 1: View setup goes in onViewCreated(), never in onCreateView().
onCreateView() inflates. onViewCreated() configures. One job each.
Rule 2: Anything that touches views uses viewLifecycleOwner, not this.
Observers, coroutine scopes, anything that can fire after the view is gone — always scope it to viewLifecycleOwner.
Fragment alive? → use lifecycleScope (this)
View alive? → use viewLifecycleOwner.lifecycleScope
When in doubt, ask: "If the user navigates away and back, will this still work correctly?" If you're using this as a lifecycle owner for view-touching code, the answer is probably no.
❓ The interview questions
Question 1 — Mid-level:
"What's the difference between
lifecycleScopeandviewLifecycleOwner.lifecycleScopein a Fragment?"
lifecycleScope is scoped to the Fragment's lifecycle — it's cancelled when the Fragment is destroyed. viewLifecycleOwner.lifecycleScope is scoped to the View's lifecycle — cancelled when the View is destroyed, which happens earlier when navigating in a back stack. For anything that interacts with views, always use the latter.
Question 2 — Senior:
"A Fragment is added to the back stack. The user navigates to a new Fragment, then presses Back. Walk me through exactly what happens to both lifecycles."
User navigates away:
Fragment: onPause → onStop → onDestroyView (Fragment stays CREATED)
View: fully destroyed
User presses Back:
Fragment: onCreateView → onViewCreated → onStart → onResume (same instance)
View: brand new View created from scratch
Follow-up the interviewer will likely ask: "So what does that mean for data you stored as view state vs instance state?"
View state (scroll position, text in EditText with android:saveEnabled="true") — lost unless you explicitly save it. Instance state (data in ViewModel, SavedStateHandle) — preserved.
Question 3 — Senior / System Design:
"How would you design Fragment communication in an app with 10+ Fragments across 3 feature modules?"
Each feature module has its own ViewModel scoped to its navigation graph. Fragments within a feature communicate through that ViewModel via navGraphViewModels(). Cross-feature communication goes through the Activity-scoped ViewModel or a shared data layer (repository + StateFlow) that both feature ViewModels observe. Fragments never reference each other directly.
// Scoped to nav graph — shared within a feature
private val featureViewModel: FeatureViewModel by navGraphViewModels(R.id.feature_graph)
// Scoped to Activity — shared across features
private val appViewModel: AppViewModel by activityViewModels()
What surprised me revisiting this
I've been writing Fragments for years. Going back carefully, two things stood out:
I still occasionally write
observe(this)out of habit. It's the kind of mistake that doesn't crash immediately — it only crashes in specific navigation patterns. Easy to miss in code review if you're not specifically looking for it.The Fragment constructor with layout ID —
Fragment(R.layout.fragment_user_profile)— means you can skip overridingonCreateView()entirely for most Fragments. I'd been overriding it by default for years without thinking about whether I needed to.
Tomorrow
Day 5 → Intent, Task and Back Stack — how Android manages navigation at the OS level, and why launchMode causes more bugs than it solves.
Have you ever shipped a viewLifecycleOwner bug to production? I have. Drop your story in the comments.
← Day 3: Activity Lifecycle — The Diagram You've Seen 100 Times
Top comments (0)