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
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
- onCreate() — Called when Activity is first created. Initialize UI, set content view, restore saved state. Only called once per Activity instance (unless recreated). Use savedInstanceState to detect recreation vs fresh start. Call super.onCreate(savedInstanceState) first.
- onStart() & onStop() — onStart: Activity becomes visible. onStop: Activity no longer visible (another Activity covers it fully or user presses Home). Register/unregister resources tied to visibility here — e.g., sensors, location updates, broadcast receivers that must only fire when visible.
- onResume() & onPause() — onResume: Activity is in foreground and interactive — user can touch it. onPause: Activity partially obscured (dialog, split screen) or about to be stopped. onPause must be FAST — the next Activity won't start until it completes. Never do heavy work in onPause.
- onDestroy() — Called before Activity is destroyed — either by user finishing it or system killing it. isFinishing() tells you which. Release non-UI resources here. If configuration change triggered onDestroy, isChangingConfigurations() returns true. ViewModel.onCleared() runs after this.
- Configuration changes — Rotating the device, changing language, resizing in multi-window — Android destroys and recreates the Activity by default. This allows loading the correct resources (layout-land, values-es) for the new configuration. The process is: onPause → onStop → onDestroy → onCreate → onStart → onResume.
- onSaveInstanceState() — Called before the Activity may be killed — called even during configuration changes. Bundle supports primitives, Parcelable, Serializable. Size limit ~1MB (shared with system). Use for transient UI state (scroll position, selected tab, user-typed text). NOT for large data — use ViewModel for that.
- onRestoreInstanceState() — Called after onStart() with the same Bundle from onSaveInstanceState. Can also restore state in onCreate from savedInstanceState parameter. onRestoreInstanceState is only called if there is a non-null Bundle — convenient for always-restore logic.
- Fragment lifecycle vs Activity lifecycle — Fragment has both Fragment lifecycle and View lifecycle (separate!). onCreateView inflates the layout, onViewCreated is where you set up views/observers. onDestroyView is called when the Fragment's view is destroyed (backstack) but Fragment instance survives. Always observe LiveData/Flow with viewLifecycleOwner, not the Fragment itself.
- Fragment backstack — addToBackStack() saves Fragment transaction to the back stack — pressing Back pops it. replace() + addToBackStack() destroys old Fragment's view but keeps it in memory on the stack. add() keeps both Fragments alive simultaneously. Overuse causes deep stacks and memory issues in large apps.
- ViewModel survives configuration changes — ViewModel lives as long as the ViewModelStore lives — which is scoped to the Activity/Fragment and survives configuration changes. On config change: Activity is destroyed but ViewModelStore is retained. New Activity instance retrieves the same ViewModel. This is the official solution to configuration change data loss.
- onSaveInstanceState vs ViewModel — ViewModel survives config changes but NOT process death. onSaveInstanceState's Bundle survives both config changes AND process death (restored when user navigates back). Combine both: ViewModel for large runtime state, SavedStateHandle for small UI state that must survive process death.
- Lifecycle-aware components — DefaultLifecycleObserver (or @OnLifecycleEvent) allows any class to observe Activity/Fragment lifecycle without subclassing. ProcessLifecycleOwner tracks app-level foreground/background. This decouples business logic from UI classes.
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. 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. 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. viewModel.workOrder.observe(this) uses 'this' (the Activity) as LifecycleOwner — correct for Activities because Activity lifecycle and view lifecycle are the same.
- 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. OrderMapFragment uses by activityViewModels() to share the ViewModel with the Activity — both see the same WorkOrder and location data.
- 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. 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. 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?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Activity Lifecycle — Android Developers (Android Developers)
- Fragment Lifecycle — Android Developers (Android Developers)
- Handle Configuration Changes (Android Developers)
- Lifecycle-aware components with LiveData (Android Developers)
- ViewLifecycleOwner vs LifecycleOwner in Fragments (Medium / Android Developers)