Fragments have a reputation for being confusing — and honestly, it’s deserved. A Fragment has two lifecycles (its own and its View’s), can outlive its View, gets recreated by the system in ways you don’t expect, and has a callback order that changes depending on how you add it. But Fragments are everywhere in modern Android — Navigation Component uses them, most multi-screen apps use them, and interview questions love them. This guide breaks down the Fragment lifecycle completely — every callback, every scenario, and the critical difference between the Fragment lifecycle and the View lifecycle.
Two Lifecycles — The Key Insight
The most important thing to understand about Fragments is that they have two separate lifecycles:
// 1. FRAGMENT LIFECYCLE — the Fragment object itself
// Created once, can survive across view destruction
// Managed by: lifecycle (this.lifecycle)
// 2. VIEW LIFECYCLE — the Fragment's View hierarchy
// Created in onCreateView, destroyed in onDestroyView
// Can be destroyed and recreated while the Fragment lives on
// Managed by: viewLifecycleOwner.lifecycle
// Why two lifecycles?
// When you navigate away from a Fragment (but keep it in the back stack),
// the VIEW is destroyed (to save memory) but the FRAGMENT OBJECT stays alive.
// When you navigate back, the SAME Fragment object creates a NEW View.
// This is why you must use viewLifecycleOwner for UI observations:
// ❌ BAD — Fragment lifecycle lives longer than the View
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { updateUI(it) } // View might be dead!
}
}
// ✅ GOOD — View lifecycle matches the View's actual lifetime
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { updateUI(it) } // safe — stops when View dies
}
}
The Complete Fragment Lifecycle
// Fragment lifecycle callbacks — in order
//
// onAttach() ← Fragment is attached to its host Activity
// ↓
// onCreate() ← Fragment is CREATED (initialise non-UI things)
// ↓
// ┌─ onCreateView() ← Create and return the View hierarchy
// │ ↓
// │ onViewCreated() ← View is ready — set up UI, observers, listeners
// │ ↓
// │ onViewStateRestored() ← Saved view state is restored (EditText text, scroll)
// │ ↓
// │ onStart() ← Fragment is VISIBLE
// │ ↓
// │ onResume() ← Fragment is INTERACTIVE
// │ ↓
// │ ———— Fragment is running ————
// │ ↓
// │ onPause() ← Fragment losing focus
// │ ↓
// │ onStop() ← Fragment no longer visible
// │ ↓
// │ onSaveInstanceState() ← Save state before potential destruction
// │ ↓
// └─ onDestroyView() ← View hierarchy is DESTROYED (Fragment may survive)
// ↓
// onDestroy() ← Fragment is being DESTROYED
// ↓
// onDetach() ← Fragment is detached from Activity
//
// The VIEW LIFECYCLE is the portion between onCreateView and onDestroyView
// The FRAGMENT LIFECYCLE is the entire chain from onAttach to onDetach
Each Callback — What to Do Where
onAttach() — Connected to Activity
// Called when the Fragment is attached to its host Activity
// The Activity is available via context or requireActivity()
override fun onAttach(context: Context) {
super.onAttach(context)
// Get references to Activity-level objects
// This is the earliest point where you can access the Activity
// Old pattern (avoid): casting Activity to an interface
// listener = context as OnItemClickListener
// Modern pattern: use shared ViewModel or Fragment Result API
}
// Key facts:
// - Called BEFORE onCreate()
// - context parameter is the host Activity
// - Don't access Views here — they don't exist yet
onCreate() — Fragment Created
// Called when the Fragment instance is created
// Initialise things that are NOT related to the View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. Process arguments
val articleId = arguments?.getString("article_id") ?: ""
// 2. Initialise ViewModel
// (can also be done as a property delegate at class level)
val viewModel: ArticleViewModel by viewModels()
// 3. Restore non-UI state
if (savedInstanceState != null) {
currentPage = savedInstanceState.getInt("current_page", 0)
}
// 4. Set up non-UI dependencies
// Don't touch Views here — they don't exist yet
}
// Key facts:
// - Called ONCE per Fragment instance (not again on view recreation)
// - savedInstanceState is null on first creation, non-null on recreation
// - Views do NOT exist yet — don't call findViewById or use binding here
// - ViewModel can be initialised here — it's not tied to the View
onCreateView() — Create the View
// Called to create the Fragment's View hierarchy
// Return the root View (or null for headless Fragments)
// With ViewBinding (recommended):
private var _binding: FragmentArticleBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentArticleBinding.inflate(inflater, container, false)
return binding.root
}
// Without ViewBinding:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_article, container, false)
}
// Key facts:
// - Return the View — do NOT add it to the container yourself
// - The second parameter (container) is the parent — pass false to attach
// - Keep this method simple — just inflate and return
// - Set up UI in onViewCreated, not here
onViewCreated() — View is Ready
// Called immediately after onCreateView returns
// The View is now ready — THIS is where you set up your UI
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 1. Set up RecyclerView, adapters, click listeners
binding.recyclerView.adapter = articleAdapter
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
// 2. Set up click listeners
binding.retryButton.setOnClickListener { viewModel.retry() }
// 3. Observe ViewModel with VIEW lifecycle (critical!)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { viewModel.uiState.collect { renderState(it) } }
launch { viewModel.events.collect { handleEvent(it) } }
}
}
// 4. Set up toolbar, menus, etc.
binding.toolbar.setNavigationOnClickListener {
findNavController().navigateUp()
}
}
// Key facts:
// - THIS is the primary setup method — not onCreate, not onCreateView
// - View is guaranteed to be non-null here
// - Always use viewLifecycleOwner for observations, not this (Fragment)
// - Called every time the View is recreated (including back stack returns)
onViewStateRestored() — State Restored
// Called after the View's saved state has been restored
// EditText content, RecyclerView scroll position, etc. are restored here
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
// The system has already restored View state (EditText text, etc.)
// You can read the restored state if needed
// Rarely overridden — system handles most view state automatically
// Use this only if you need to react AFTER view state restoration
}
// Key facts:
// - Called after onViewCreated and before onStart
// - System automatically restores View state for Views with android:id
// - Called EVERY TIME the view is recreated
onStart() / onResume() / onPause() / onStop()
// These mirror the Activity lifecycle callbacks
override fun onStart() {
super.onStart()
// Fragment is VISIBLE — same as Activity.onStart
}
override fun onResume() {
super.onResume()
// Fragment is INTERACTIVE — same as Activity.onResume
}
override fun onPause() {
super.onPause()
// Fragment losing focus — same as Activity.onPause
// Keep it fast
}
override fun onStop() {
super.onStop()
// Fragment no longer visible — safe for heavier cleanup
}
// In practice, you rarely override these directly in modern Android
// Use viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) instead
// It automatically starts/stops with onStart/onStop
onSaveInstanceState() — Save State
// Save small UI state before the Fragment might be destroyed
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("selected_tab", binding.tabLayout.selectedTabPosition)
outState.putString("search_query", currentQuery)
}
// Restore in onCreate (Fragment state) or onViewCreated (View state)
// Same rules as Activity: keep the Bundle small (<1 MB)
onDestroyView() — View Dies, Fragment May Survive
// Called when the View hierarchy is destroyed
// The Fragment object may STILL BE ALIVE (in the back stack)
override fun onDestroyView() {
super.onDestroyView()
// 1. CRITICAL: null out the binding reference to avoid memory leaks
_binding = null
// 2. Clear any View references
// adapter = null (if holding a reference)
// 3. Cancel any View-specific work
}
// Why null the binding?
// If the Fragment is on the back stack, the Fragment object is alive
// but the Views are destroyed. If binding still holds View references,
// those Views can't be garbage collected = MEMORY LEAK
// Key facts:
// - Called when the View is destroyed (navigating away, back stack)
// - Fragment object may survive — it's still in memory
// - ViewModel survives — same instance when View is recreated
// - ALWAYS set _binding = null here with ViewBinding
onDestroy() / onDetach()
override fun onDestroy() {
super.onDestroy()
// Fragment object is being destroyed
// Clean up anything tied to the Fragment itself (not the View)
}
override fun onDetach() {
super.onDetach()
// Fragment is detached from the Activity
// requireActivity() will throw after this point
// Last callback — Fragment is fully gone
}
// In modern Android, you rarely override these
// ViewModel handles data cleanup (cleared via viewModelScope)
// ViewBinding is nulled in onDestroyView
// Lifecycle-aware components clean up automatically
Real Scenarios — Exact Callback Order
Scenario 1: Fragment added for the first time
// Fragment is added via NavController or FragmentTransaction
// onAttach → onCreate → onCreateView → onViewCreated
// → onViewStateRestored → onStart → onResume
Scenario 2: Navigate to another Fragment (current goes to back stack)
// Fragment A → navigate to Fragment B (A stays in back stack)
// Fragment A:
// onPause → onStop → onSaveInstanceState → onDestroyView
// (Fragment A's View is DESTROYED, but Fragment A object SURVIVES)
// Fragment B:
// onAttach → onCreate → onCreateView → onViewCreated
// → onViewStateRestored → onStart → onResume
// Key insight: Fragment A's VIEW is destroyed to save memory
// But Fragment A itself (and its ViewModel) are alive in the back stack
Scenario 3: Press Back — return to previous Fragment
// Fragment B → press Back → return to Fragment A
// Fragment B:
// onPause → onStop → onDestroyView → onDestroy → onDetach
// (Fragment B is completely gone)
// Fragment A:
// onCreateView → onViewCreated → onViewStateRestored → onStart → onResume
// (Fragment A creates a NEW View but uses the SAME Fragment object)
// Notice: Fragment A's onCreate is NOT called again
// The Fragment object was alive the whole time — only the View was recreated
// ViewModel data is still there — no re-fetch needed
Scenario 4: Screen rotation (configuration change)
// Activity is destroyed and recreated — all Fragments go with it
// Old Fragment instance:
// onPause → onStop → onSaveInstanceState → onDestroyView → onDestroy → onDetach
// New Fragment instance (system recreates it):
// onAttach → onCreate(savedInstanceState) → onCreateView → onViewCreated
// → onViewStateRestored → onStart → onResume
// ViewModel SURVIVES — same instance, new Fragment and View
// savedInstanceState has data from onSaveInstanceState
Scenario 5: Activity goes to background (Home button)
// User presses Home
// Fragment: onPause → onStop
// User returns
// Fragment: onStart → onResume
// View is NOT destroyed — still in memory
// Fragment object is still alive
Scenario 6: Process death while in background
// System kills the app while in background
// onPause → onStop → onSaveInstanceState → (process killed)
// User returns — system recreates everything:
// Activity: onCreate(savedInstanceState)
// Fragment: onAttach → onCreate(savedInstanceState) → onCreateView
// → onViewCreated → onViewStateRestored → onStart → onResume
// ViewModel is NEW — previous data is lost
// SavedStateHandle data is restored
// Navigation state is restored — user is on the same screen
Fragment vs Activity Lifecycle — Side by Side
// ┌───────────────────────┬────────────────────────┐
// │ Activity │ Fragment │
// ├───────────────────────┼────────────────────────┤
// │ │ onAttach() │
// │ onCreate() │ onCreate() │
// │ │ onCreateView() │
// │ │ onViewCreated() │
// │ │ onViewStateRestored() │
// │ onStart() │ onStart() │
// │ onResume() │ onResume() │
// │ ——— running ——— │ ——— running ——— │
// │ onPause() │ onPause() │
// │ onStop() │ onStop() │
// │ onSaveInstanceState() │ onSaveInstanceState() │
// │ │ onDestroyView() │
// │ onDestroy() │ onDestroy() │
// │ │ onDetach() │
// └───────────────────────┴────────────────────────┘
//
// Fragment has EXTRA callbacks:
// onAttach/onDetach — connection to Activity
// onCreateView/onDestroyView — View lifecycle (can happen independently)
// onViewCreated — safe place to set up UI
// onViewStateRestored — after system restores View state
Passing Data to Fragments
Arguments — the right way
// Pass data via arguments Bundle — survives recreation
// Creating with arguments
class ArticleFragment : Fragment(R.layout.fragment_article) {
private val articleId: String by lazy {
requireArguments().getString("article_id", "")
}
companion object {
fun newInstance(articleId: String) = ArticleFragment().apply {
arguments = bundleOf("article_id" to articleId)
}
}
}
// With Navigation Component (recommended):
// Arguments are defined in nav_graph.xml and passed via SafeArgs
// val args: ArticleFragmentArgs by navArgs()
// val articleId = args.articleId
Fragment Result API — communicating between Fragments
// Fragment A wants a result from Fragment B
// Fragment A — set up listener
setFragmentResultListener("filter_request") { _, bundle ->
val selectedFilter = bundle.getString("filter_value", "all")
applyFilter(selectedFilter)
}
// Fragment B — send result back
setFragmentResult("filter_request", bundleOf("filter_value" to "recent"))
findNavController().navigateUp()
// This replaces the old targetFragment pattern (deprecated)
// Works across configuration changes and process death
ViewBinding in Fragments — The Pattern
class ArticleFragment : Fragment(R.layout.fragment_article) {
// Nullable backing field — null when View doesn't exist
private var _binding: FragmentArticleBinding? = null
// Non-null accessor — only use between onViewCreated and onDestroyView
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentArticleBinding.bind(view)
// Now safe to use binding
binding.toolbar.title = "Articles"
binding.recyclerView.adapter = adapter
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null // CRITICAL — prevents memory leak
}
}
// Why this pattern?
// Fragment can outlive its View (back stack)
// If _binding holds View references after onDestroyView,
// those Views can't be garbage collected = memory leak
// Setting _binding = null in onDestroyView fixes this
Common Mistakes to Avoid
Mistake 1: Using Fragment lifecycle instead of View lifecycle for observations
// ❌ Fragment lifecycle outlives the View — crashes or duplicate observations
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { binding.textView.text = it }
// After navigating away and back, this runs TWICE
// because the Fragment was alive the whole time
}
}
// ✅ View lifecycle matches the View
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { binding.textView.text = it }
// Properly cancelled when View is destroyed
// Restarted when new View is created
}
}
Mistake 2: Not nulling out ViewBinding in onDestroyView
// ❌ Memory leak — binding holds references to destroyed Views
override fun onDestroyView() {
super.onDestroyView()
// forgot _binding = null!
}
// Fragment is on back stack, Views are destroyed
// But binding still references them → Views can't be GC'd
// ✅ Always null out binding
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
Mistake 3: Accessing Views in onCreate
// ❌ Views don't exist yet in onCreate — crash!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.textView.text = "Hello" // 💥 View not created yet
}
// ✅ Access Views in onViewCreated
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textView.text = "Hello" // ✅ View exists
}
Mistake 4: Creating Fragment with constructor parameters
// ❌ System uses no-arg constructor to recreate — your params are lost
class ArticleFragment(private val articleId: String) : Fragment() {
// After rotation: system calls ArticleFragment() — no articleId! 💥
}
// ✅ Use arguments Bundle — survives recreation
class ArticleFragment : Fragment() {
private val articleId by lazy { requireArguments().getString("article_id", "") }
companion object {
fun newInstance(articleId: String) = ArticleFragment().apply {
arguments = bundleOf("article_id" to articleId)
}
}
}
// ✅ Even better with Navigation SafeArgs — compile-time safe
Mistake 5: Using requireContext() or requireActivity() in callbacks after detach
// ❌ Crash if Fragment is detached when callback fires
viewModel.result.observe(this) { result ->
Toast.makeText(requireContext(), result, Toast.LENGTH_SHORT).show()
// 💥 IllegalStateException if Fragment is already detached
}
// ✅ Check if Fragment is attached, or use viewLifecycleOwner
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.result.collect { result ->
// Safe — only runs when View is alive and Fragment is attached
Toast.makeText(requireContext(), result, Toast.LENGTH_SHORT).show()
}
}
}
Summary
- Fragments have two lifecycles: Fragment lifecycle (onAttach→onDetach) and View lifecycle (onCreateView→onDestroyView)
- Always use viewLifecycleOwner for UI observations — never the Fragment’s own lifecycle
- onViewCreated is where you set up UI, click listeners, and ViewModel observers
- Always null out ViewBinding in onDestroyView to prevent memory leaks
- When navigating away, the View is destroyed but the Fragment survives on the back stack
- When navigating back, the same Fragment creates a new View (onCreate is NOT called again)
- Pass data via arguments Bundle or Navigation SafeArgs — never constructor parameters
- Use Fragment Result API for communication between Fragments (replaces targetFragment)
- Fragment callbacks mirror Activity callbacks but add: onAttach, onCreateView, onViewCreated, onDestroyView, onDetach
- Configuration change destroys and recreates both Fragment and View — ViewModel survives
- Process death destroys everything — only onSaveInstanceState/SavedStateHandle data survives
- Use repeatOnLifecycle(STARTED) with viewLifecycleOwner instead of manually managing start/stop
- Never create Fragments with constructor parameters — the system uses a no-arg constructor on recreation
The Fragment lifecycle is harder than the Activity lifecycle because of the two-lifecycle design. But once you internalise the rule — “use viewLifecycleOwner for anything UI-related” — most of the complexity melts away. The View lifecycle is what matters for your day-to-day code. The Fragment lifecycle is what matters for understanding why things work the way they do.
Happy coding!
Comments (0)