Android Internals · Complete Guide

Master Android
Lifecycle

A comprehensive deep dive into every component lifecycle in Android — from Application to Jetpack Compose. Understand how Android manages your app's state, memory, and navigation.

Application Activity Fragment ViewModel Compose Service LiveData Process

What is the Application class?

The Application class is a base class for maintaining global application state. Android instantiates it before any other component in your app's process. There is exactly one Application instance per process — it persists for the entire lifetime of that process.

You extend it to perform global initializations: dependency injection containers (Dagger/Hilt), crash reporting SDKs, analytics, database initialization, and anything that must exist before the first Activity appears.

attachBaseContext(Context)
Called very first — before onCreate. Use for MultiDex installation or custom ContextWrapper. The base context is attached here.
onCreate()
The primary initialization point. Called when the application starts. Initialize global singletons, inject dependency graphs, start crash reporting. Keep it fast — this blocks the first screen from appearing.
onConfigurationChanged()
Called when device config changes (rotation, locale). Rare use at the Application level — most config handling is in Activity.
onLowMemory() / onTrimMemory(level)
System signals memory pressure. onTrimMemory gives granular levels: TRIM_MEMORY_UI_HIDDEN, RUNNING_LOW, RUNNING_CRITICAL. Release caches here.
Process terminated
There is no onDestroy for Application. The process is killed by the OS — no callback fires. This is why critical data must be persisted before this point.
Kotlin · MyApp.kt
@HiltAndroidApp
class MyApp : Application() {

    override fun attachBaseContext(base: Context) {
        super.attachBaseContext(base)
        // MultiDex or custom wrapper goes here
    }

    override fun onCreate() {
        super.onCreate()
        // Hilt injects the dependency graph automatically via @HiltAndroidApp
        Timber.plant(Timber.DebugTree())
        FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
    }

    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        when (level) {
            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> imageCache.evictAll()
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> networkCache.evictAll()
        }
    }
}
Common mistake: Storing activity or view references in the Application class. Since Application outlives all activities, this causes memory leaks. Only store truly global, context-independent state here.

Application vs. Process

One process can have multiple applications if you specify android:process in the manifest. Conversely, a single app can run in multiple processes — each gets its own Application instance. The Application class is per-process, not per-app.

Good uses
What belongs here
DI graph initialization, crash/analytics SDK setup, global WorkManager config, ProcessLifecycleOwner observers, app-wide font loading.
Avoid
What doesn't belong
Activity/View references, heavy synchronous I/O, large data structures that should be scoped to a screen, anything that requires a UI context.

The 7 core callbacks

Every Activity moves through a defined set of states. The system calls these methods to signal transitions. Implementing them correctly is the difference between an app that feels solid and one that leaks memory, loses user data, or crashes on rotation.

Activity launched
intent received
onCreate(savedInstanceState)
Created
UI initialized
onStart()
Started
visible, not interactive
onResume()
Resumed ★
foreground · interactive
onPause()
Paused
partially visible
onStop()
Stopped
invisible · killable
onDestroy()
Destroyed
removed from memory

Deep dive: each callback

onCreate() — The setup callback

Called once when the Activity is first created, or after a process kill when the user returns. Receives a savedInstanceState Bundle — null on first launch, non-null when restoring. This is where you call setContentView(), initialize ViewModels, set up RecyclerView adapters, and restore saved UI state.

Key rule: Never do heavy work synchronously in onCreate. Database queries, network calls, and large computations block the main thread and cause ANRs. Use coroutines or background threads.

onStart() — Becoming visible

The Activity is about to become visible. At this point it's not yet interactive. This is a good place to register BroadcastReceivers that are only needed when the Activity is visible, or to begin animating UI elements that are always shown.

onResume() — Taking focus

The Activity is now fully visible, in the foreground, and receiving user input. This is the "running" state. Acquire exclusive resources here: camera, microphone, sensors. Resume paused animations. This callback is called every time the Activity comes to the foreground — including when returning from another Activity or dismissing a dialog.

onPause() — Losing focus (fast!)

Called when the Activity is partially obscured — another Activity in multi-window, a dialog, or the system about to transition away. Must complete very quickly — the next Activity cannot start until onPause returns. Release camera/mic here. Save critical but lightweight state. Do NOT do disk I/O, network calls, or complex animations.

Critical: onSaveInstanceState is NOT called from onPause. It's called before onStop. Do not try to save Bundle state in onPause — use Room, SharedPreferences, or the ViewModel for durable saves, and the Bundle mechanism for transient UI state.

onStop() — Invisible

The Activity is completely hidden. This is where you release expensive resources: database cursors, location updates, expensive animations. The system may call onSaveInstanceState just before this if it may need to restore the Activity later. After onStop the process can be killed without further callbacks — this makes onStop the last guaranteed callback for saves.

onRestart() — Coming back from stopped

Called when an Activity that was stopped (but not destroyed) is about to start again. Followed immediately by onStart. Use this to refresh data that might have changed while the user was away — for example, re-querying a contact that might have been edited in another app.

onDestroy() — Cleanup

Called before the Activity is destroyed. This happens either because the user finished the Activity, or because the system is destroying it due to a configuration change. Check isFinishing() to distinguish. Release any remaining resources, close threads, unregister any remaining listeners. Not guaranteed to be called if the process is killed — never rely on it for critical saves.

Kotlin · MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by viewModels()
    private var camera: Camera? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ViewModel survives rotation — data already loaded
        viewModel.uiState.observe(this) { state -> render(state) }

        // Restore transient UI state (e.g. scroll position)
        savedInstanceState?.let {
            binding.recycler.scrollToPosition(it.getInt("scroll_pos", 0))
        }
    }

    override fun onResume() {
        super.onResume()
        camera = Camera.open()  // acquire exclusive resource
    }

    override fun onPause() {
        super.onPause()
        camera?.release()   // must release before next Activity can use camera
        camera = null
    }

    override fun onStop() {
        super.onStop()
        // Heavy resource release, last guaranteed callback
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt("scroll_pos", binding.recycler.computeVerticalScrollOffset())
        outState.putString("search_query", binding.searchView.query.toString())
    }

    override fun onDestroy() {
        super.onDestroy()
        if (isFinishing()) {
            // Permanent destruction — user navigated away
        } else {
            // Config change (rotation) — ViewModel will re-attach
        }
    }
}

Configuration changes & rotation

Screen rotation triggers a full destroy-recreate cycle by default. The system calls onSaveInstanceState, destroys the Activity, then creates a new one with the saved Bundle. The ViewModel is kept alive across this — it's the primary tool for surviving rotation without saving/restoring complex data through a Bundle.

User rotates
onPause()
onStop()
onSaveInstance State()
onDestroy()
onCreate(bundle)
Restored ✓

Fragment vs Activity lifecycle

A Fragment has two lifecycles: its own (getLifecycle()) and its view's (getViewLifecycleOwner()). The fragment can exist without a view — when it's on the back stack, the view is destroyed but the fragment object lives on. This is the single most important distinction to understand.

Fragment lifecycle
Fragment object alive
Persists even when on the back stack. Use this for data observation, ViewModel acquisition, and non-view state. Never reference views here.
View lifecycle
View hierarchy alive
Created in onCreateView, destroyed in onDestroyView. Always use viewLifecycleOwner for LiveData observation to avoid stale observers.
onAttach(context)
Fragment is attached to its host Activity. Get a reference to the Activity here. The fragment has no view yet.
onCreate(savedInstanceState)
Initialize non-view data, acquire ViewModels. Do NOT reference binding here — the view doesn't exist yet.
onCreateView(inflater, container, savedInstanceState)
Inflate and return the Fragment's view. This is the first point where you can create ViewBinding. Return null if your Fragment has no UI.
onViewCreated(view, savedInstanceState)
The view is fully created. Set up click listeners, RecyclerViews, and observe LiveData via viewLifecycleOwner here. This is your primary setup method.
onViewStateRestored(savedInstanceState)
View state has been restored. Called after onViewCreated. Useful when you need the view hierarchy fully restored before doing work.
onStart() / onResume()
Mirrors Activity callbacks. Fragment is visible and interactive. ViewLifecycle is STARTED/RESUMED.
onPause() / onStop()
Fragment losing visibility. Mirrors Activity. Release interactive resources.
onDestroyView()
View is destroyed (Fragment on back stack). Null your binding reference here to avoid memory leaks. ViewLifecycleOwner becomes DESTROYED — observers are automatically removed.
onDestroy() / onDetach()
Fragment object is about to be destroyed and detached from Activity. Final cleanup.
Memory leak trap: Storing a binding reference in a field without nulling it in onDestroyView. The view is destroyed but your field keeps it alive. Always: private var _binding: FragmentFooBinding? = null and null it in onDestroyView.
Kotlin · ProfileFragment.kt
class ProfileFragment : Fragment(R.layout.fragment_profile) {

    private val viewModel: ProfileViewModel by viewModels()
    private var _binding: FragmentProfileBinding? = null
    private val binding get() = _binding!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ViewModel acquired here — survives config changes
        // No view references!
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentProfileBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Use viewLifecycleOwner — NOT 'this'
        viewModel.profile.observe(viewLifecycleOwner) { profile ->
            binding.nameText.text = profile.name
        }

        binding.editBtn.setOnClickListener {
            findNavController().navigate(R.id.action_profile_to_edit)
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null  // ← critical: prevents memory leak
    }
}

Why viewLifecycleOwner matters

If you observe LiveData with this (the Fragment as LifecycleOwner), the observer lives as long as the Fragment object. When the Fragment goes on the back stack, its view is destroyed but the fragment lives on. When the user navigates back, a new view is created and onViewCreated runs again — adding a second observer. Now you have two observers for the same LiveData, and UI updates fire twice. Using viewLifecycleOwner ties the observer to the view's lifecycle, so it's automatically removed in onDestroyView.

How ViewModel survives rotation

ViewModels are stored in a ViewModelStore, which is retained by the system during configuration changes. When you call ViewModelProvider(this)[MyViewModel::class.java], you're either creating a new ViewModel (first call) or getting the existing one from the retained store. The ViewModel's onCleared() is only called when the owning scope is truly finished — not during rotation.

Activity A (portrait)
ViewModel created
Rotation
Activity destroyed
ViewModelStore
retained by system
Activity A (landscape)
same ViewModel instance
Activity finished
onCleared() called

ViewModel scoping

ViewModels can be scoped to different lifecycle owners. This determines their lifetime and which components can share the same instance.

Scope Lifetime Shared between How to get
Activity Until activity finishes All fragments in that activity by activityViewModels()
Fragment Until fragment detaches That fragment only by viewModels()
NavGraph Until nav graph is popped Fragments in that subgraph by navGraphViewModels(R.id.graph)
Custom Custom owner lifetime Explicit owner ViewModelProvider(owner)[...]
Kotlin · MainViewModel.kt
class MainViewModel(private val repo: UserRepository) : ViewModel() {

    private val _uiState = MutableStateFlow(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    val users = repo.getAllUsers()  // Flow from Room — lifecycle-aware

    private val job: Job

    init {
        // viewModelScope is cancelled in onCleared()
        job = viewModelScope.launch {
            try {
                val result = repo.fetchUsers()
                _uiState.value = UiState.Success(result)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message)
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        // viewModelScope auto-cancels — but cancel explicit jobs here
        // Called ONLY when Activity/Fragment is truly finished (not rotation)
    }
}
viewModelScope: Always launch coroutines in viewModelScope. It's automatically cancelled when onCleared() fires, preventing leaks from in-flight network calls after the user has left the screen.
ViewModel does NOT survive process kill. Only the Bundle (via onSaveInstanceState) crosses a process kill boundary. For values that must survive process death, use SavedStateHandle — it's injected into ViewModels by Hilt and backed by the Bundle mechanism.

SavedStateHandle — bridging ViewModel and Bundle

SavedStateHandle is a key-value map that stores primitive and Parcelable values, and is serialized into the saved state Bundle. It lets a ViewModel hold state that survives both configuration changes AND process kill — without the ViewModel needing to know about the Activity/Fragment lifecycle directly.

Kotlin · SearchViewModel.kt
class SearchViewModel(
    private val savedState: SavedStateHandle,
    private val repo: SearchRepository
) : ViewModel() {

    // Survives rotation AND process kill
    var query: String
        get() = savedState["query"] ?: ""
        set(value) { savedState["query"] = value }

    val results: StateFlow<List<Result>> =
        savedState.getStateFlow("query", "")
            .flatMapLatest { q -> repo.search(q) }
            .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}

The Composition lifecycle

A composable's lifecycle has three phases: entering the Composition (initial call), recomposing (subsequent calls when inputs change), and leaving the Composition (removed from the tree). There are no inherited lifecycle methods — state and effects are expressed through Compose's built-in APIs.

Enter Composition
onActive (LaunchedEffect)
State changes
Recomposition
Leave Composition
onDispose (DisposableEffect)

Side effects in Compose

Because composable functions may run at any time and multiple times, side effects (network calls, subscriptions, analytics) must be handled through structured effect APIs that tie their lifecycle to the Composition.

LaunchedEffect(key)
Coroutine tied to composition
Launches a coroutine when the composable enters the Composition (or when keys change). Automatically cancelled when the composable leaves or keys change. Use for: data loading, animations, snackbar events.
DisposableEffect(key)
Setup + cleanup pair
Run code when entering and provide an onDispose cleanup block. Use for: registering/unregistering listeners, lifecycle observers, MapView setup.
SideEffect
Every recomposition
Runs after every successful recomposition. Use to push Compose state into non-Compose code — e.g. updating a legacy View or analytics system with the current state.
rememberCoroutineScope
User-triggered effects
Get a CoroutineScope tied to the composition. Use to launch coroutines in response to user events (button clicks) — LaunchedEffect is for automatic triggers, this is for manual ones.
Kotlin · Compose effects
@Composable
fun UserProfile(userId: String, viewModel: ProfileViewModel) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    // Reload when userId changes — coroutine cancelled on leave
    LaunchedEffect(userId) {
        viewModel.loadUser(userId)
    }

    // Listen to a callback-based API
    DisposableEffect(userId) {
        val listener = PresenceListener { online -> viewModel.setOnline(online) }
        PresenceApi.register(userId, listener)
        onDispose { PresenceApi.unregister(userId, listener) }
    }

    // Runs after EVERY recomposition — push state to analytics
    SideEffect {
        Analytics.setCurrentUser(uiState.userId)
    }

    val scope = rememberCoroutineScope()

    Button(onClick = {
        // User-triggered: use scope, not LaunchedEffect
        scope.launch { viewModel.saveProfile() }
    }) { Text("Save") }
}

Recomposition — what triggers it

A composable recomposes when its inputs (parameters or read State objects) change. Compose is smart — it tracks which composables read which state objects and only recomposes the minimal subtree necessary.

Counter(count = 5) recomposed count changed from 4 → 5
StaticTitle("Hello") skipped inputs didn't change
onClick = { ... } unstable lambda new instance each composition
remember { … } stable value preserved across recompositions

Compose and Activity/Fragment lifecycle

Compose composables observe the Android lifecycle through LocalLifecycleOwner. When collecting flows, use collectAsStateWithLifecycle() (from lifecycle-runtime-compose) instead of collectAsState() — it automatically stops collection when the host lifecycle drops below STARTED, preventing work when the app is in the background.

Kotlin · Lifecycle-aware collection
@Composable
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {

    // ✓ Stops collecting when app goes to background
    val state by viewModel.uiState.collectAsStateWithLifecycle()

    // ✗ Keeps collecting even when invisible — wastes battery
    // val state by viewModel.uiState.collectAsState()

    // Observe the host lifecycle directly
    val lifecycle = LocalLifecycleOwner.current.lifecycle
    DisposableEffect(lifecycle) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_RESUME) viewModel.refresh()
        }
        lifecycle.addObserver(observer)
        onDispose { lifecycle.removeObserver(observer) }
    }
}

Three service types

Type Lifecycle Killed by system? Use case
Started onCreate → onStartCommand → onDestroy Maybe Fire-and-forget background tasks
Bound onCreate → onBind → onUnbind → onDestroy When no clients IPC between components
Foreground Started + persistent notification Rarely Music, navigation, downloads
Kotlin · MusicPlayerService.kt
class MusicPlayerService : Service() {

    private val binder = LocalBinder()
    private lateinit var mediaPlayer: MediaPlayer

    override fun onCreate() {
        super.onCreate()
        mediaPlayer = MediaPlayer()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val notification = buildNotification()
        startForeground(NOTIF_ID, notification)  // promote to foreground
        mediaPlayer.start()
        return START_STICKY  // restart if killed, don't re-deliver intent
    }

    override fun onBind(intent: Intent): IBinder = binder

    inner class LocalBinder : Binder() {
        fun getService() = this@MusicPlayerService
    }

    override fun onDestroy() {
        super.onDestroy()
        mediaPlayer.stop()
        mediaPlayer.release()
    }
}
Modern alternative: For most background work, prefer WorkManager (guaranteed execution with constraints) over raw Services. Reserve Services for long-running user-facing operations that need a notification (music, navigation, file upload) or for binding between app components.

LiveData

LiveData is lifecycle-aware — it only delivers updates to observers that are in the STARTED or RESUMED state. When the observer goes to STOPPED, updates are queued. When it resumes, the latest value is delivered. When the observer is DESTROYED, the observation is automatically removed.

Feature LiveData StateFlow SharedFlow
Lifecycle aware ✓ built-in ✓ with collectAsStateWithLifecycle ✓ with collectAsStateWithLifecycle
Initial value required No Yes No
Replays last value Yes Yes (1) Configurable
Coroutine friendly Limited Native Native
One-shot events Problematic Problematic Yes (replay=0)
Kotlin · Modern reactive patterns
class FeedViewModel(repo: FeedRepository) : ViewModel() {

    // StateFlow — current state, always has a value
    val feedState: StateFlow<FeedState> = repo.getFeed()
        .map { FeedState.Success(it) }
        .catch { emit(FeedState.Error(it.message)) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000), // stays alive 5s after last subscriber
            initialValue = FeedState.Loading
        )

    // SharedFlow — one-shot events (navigation, snackbar)
    private val _events = MutableSharedFlow<UiEvent>()
    val events: SharedFlow<UiEvent> = _events

    fun onLikeClicked(postId: String) {
        viewModelScope.launch {
            repo.likePost(postId)
            _events.emit(UiEvent.ShowSnackbar("Liked!"))
        }
    }
}

// In the Fragment:
viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        launch { viewModel.feedState.collect { render(it) } }
        launch { viewModel.events.collect { handleEvent(it) } }
    }
}
WhileSubscribed(5000): This magic number keeps the upstream flow alive for 5 seconds after the last subscriber unsubscribes. This means a rotation (which briefly drops all subscribers) doesn't restart the upstream data fetch — the ViewModel reuses the cached result.

Process lifecycle

Android assigns each process an importance level and kills lower-priority processes when memory is needed. Your process can be killed without any callbacks if it's not in the foreground. The importance hierarchy:

Foreground process
Has a resumed Activity, or a Service bound to a resumed Activity, or a started foreground Service. Never killed under normal conditions.
Visible process
Activity is visible but not in foreground (paused). Kept alive unless absolutely necessary to kill. Extremely rare to be killed.
Service process
Running a started Service not visible to user. Kept alive for hours, but killed under sustained memory pressure.
Cached (background) process
No visible components. Kept for fast app switching but freely killed. Your stopped Activities live here. State bundle is preserved by the system.
Empty process
No active components. Killed immediately or within seconds. Used only as a warm shell for fast restart.

The back stack (Task)

A Task is a collection of Activities arranged in a stack. When you start Activity B from A, B goes on top. Back press pops B and returns to A. Tasks can be in the foreground (visible) or background (cached). Multiple tasks can exist — switching apps means switching tasks.

Activity A · MainActivity bottom
Activity B · DetailActivity ←back→
Activity C · EditActivity TOP · Resumed

Launch modes

The android:launchMode manifest attribute and Intent flags control how Activities are added to the back stack:

Launch mode Behavior Common use
standard New instance every time, stacked Default — most Activities
singleTop Reuse if already at top (onNewIntent) Notification deep links
singleTask One instance in task, clears above Main/home screen
singleInstance Own private task, one instance System-level screens, Dialer

The three survival scenarios

Storage Rotation Process kill + return App uninstall Size limit
ViewModel Unlimited
Instance state (Bundle) ~1MB
SharedPreferences Unlimited
Room (SQLite) Device storage
DataStore Practical unlimited

The complete save & restore flow

Kotlin · Complete state management pattern
// ViewModel: survives rotation, holds heavy data
class FormViewModel(private val handle: SavedStateHandle) : ViewModel() {

    // SavedStateHandle: survives process kill
    var draftTitle: String
        get() = handle["draft_title"] ?: ""
        set(v) { handle["draft_title"] = v }

    // Complex data: in ViewModel memory (rotation), Room (process kill)
    val draftItems: StateFlow<List<Item>> = handle
        .getStateFlow("draft_id", "")
        .flatMapLatest { id -> draftRepo.getDraft(id) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}

// Activity: saves ephemeral UI state (scroll, selection)
class FormActivity : AppCompatActivity() {

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        // Only ephemeral UI state not in ViewModel
        outState.putInt("tab_index", binding.tabs.selectedTabPosition)
        outState.putBoolean("keyboard_open", binding.searchBar.hasFocus())
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_form)

        savedInstanceState?.let { bundle ->
            binding.tabs.getTabAt(bundle.getInt("tab_index", 0))?.select()
        }
    }
}
Decision rule: Does this data belong to a domain entity? → Room/DataStore. Is it transient UI position state? → Bundle. Is it expensive to recompute and only needed while the app is alive? → ViewModel. Does it need to survive process death and be scoped to a ViewModel? → SavedStateHandle.
Callback Component When called Do this Don't do this
onCreate() Activity Fragment First creation or after process kill setContentView, init ViewModel, restore state Heavy I/O, network calls
onStart() Activity Fragment Becoming visible Register visible-only receivers Exclusive resources (camera)
onResume() Activity Fragment Taking foreground focus Acquire camera/mic, resume animations Slow operations
onPause() Activity Fragment Losing focus (must be FAST) Release camera/mic, save critical state Disk I/O, heavy work
onStop() Activity Fragment Becoming invisible Release heavy resources, pause sensors Starting new activities
onSaveInstanceState() Activity Fragment Before onStop when state may need saving Save ephemeral UI state to Bundle Save domain data here
onDestroyView() Fragment View hierarchy destroyed (back stack) Null _binding reference Reference binding after this
onDestroy() Activity Fragment Activity/Fragment being destroyed Final cleanup, check isFinishing() Rely on this for critical saves
onCleared() ViewModel Owner scope truly finished Cancel non-viewModelScope coroutines Trust this for process-kill saves
LaunchedEffect / DisposableEffect Compose Enter/leave composition, key changes Side effects, subscriptions UI work outside recomposition
That's the complete picture.

Android lifecycle is complex because the OS must manage memory aggressively for billions of devices. Every callback exists for a reason — respect its contract and your apps will be solid.