---
title: Fragments
source: 'https://academia.sh/en/courses/android-platform-and-tools/fragments'
course: 'Android Platform and Tools'
language: en
updated: '2026-10-21T05:02:35+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Defines a fragment as a reusable UI section inside an activity'
    - 'Measures that a fragment transaction stays pending while the host activity is stopped'
    - 'Shows that the back button gives a substitute result for a transaction not added to the back stack'
    - 'Verifies that the same transaction receives three different fulfillments after the state is saved'
---

# Fragments

Measures where a fragment sits inside an activity, how FragmentManager transactions work, and how a fragment transaction is fulfilled while the host activity is stopped or has already saved its state.

The bundle an activity saves is small, and if the whole screen is loaded into a single activity, every change affects the entire screen. A fragment splits that load. A fragment is a reusable UI section that lives inside an activity and has its own UI and its own lifecycle. A list screen and a detail screen can be two fragments; on a phone they are shown one after the other in the same activity, on a large screen side by side.

The life of a fragment depends on its host activity. The question of this lesson is therefore related to the previous one: when a fragment transaction is requested, how does the platform **fulfill** that request according to the state of the host?

```kotlin
// Fulfillment.kt — classifies how the platform fulfills a request
sealed interface Outcome {
    data class Given(val value: String) : Outcome
    data class Waiting(val on: String) : Outcome
    data class Refused(val reason: String) : Outcome
}

class Ask(val label: String, val wanted: String, val outcome: Outcome) {
    val fulfillment: String = when (outcome) {
        is Outcome.Waiting -> "pending"
        is Outcome.Refused -> "unmet"
        is Outcome.Given -> if (outcome.value == wanted) "full" else "substitute"
    }

    fun report() {
        val detail = when (outcome) {
            is Outcome.Given -> "given=${outcome.value}"
            is Outcome.Waiting -> "waiting-on=${outcome.on}"
            is Outcome.Refused -> "reason=${outcome.reason}"
        }
        println("$label: wanted=$wanted | $detail -> $fulfillment")
    }
}

fun tally(asks: List<Ask>) {
    for (name in listOf("full", "substitute", "pending", "unmet")) {
        println("$name: ${asks.count { it.fulfillment == name }}")
    }
}
```

## FragmentManager Transactions

Fragments are managed with `FragmentManager`. A transaction starts with `beginTransaction`, consists of one or several steps, and ends with `commit`. The most common steps are `add` and `replace`: the first adds a fragment to a container, the second swaps the fragment in the container for another. If the transaction is added to the back stack with `addToBackStack`, the back button undoes it.

```kotlin
// Platform code: adds a fragment to the container and records it on the back stack
supportFragmentManager.beginTransaction()
    .replace(R.id.container, DetailFragment())
    .addToBackStack(null)
    .commit()
```

The model sets up this rule of the platform. `commit` does not apply the transaction at once; it looks at the state of the host. If the host is started, the transaction is applied. If the host is stopped, the platform queues the transaction and holds it until the host is started again. The model builds this order from two inputs: the name of the transaction and the state of the host.

```kotlin
// Main.kt — fragment transaction by host state
fun commit(op: String, hostState: String): Outcome =
    if (hostState == "stopped") Outcome.Waiting("host-started") else Outcome.Given(op)

fun main() {
    val add = "add:ListFragment"
    val replace = "replace:DetailFragment"
    val asks = listOf(
        Ask("add-foreground", add, commit(add, "resumed")),
        Ask("replace-foreground", replace, commit(replace, "resumed")),
        Ask("add-stopped", add, commit(add, "stopped")),
        Ask("replace-stopped", replace, commit(replace, "stopped")),
    )
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
}
```

```text
add-foreground: wanted=add:ListFragment | given=add:ListFragment -> full
replace-foreground: wanted=replace:DetailFragment | given=replace:DetailFragment -> full
add-stopped: wanted=add:ListFragment | waiting-on=host-started -> pending
replace-stopped: wanted=replace:DetailFragment | waiting-on=host-started -> pending
full: 2
substitute: 0
pending: 2
unmet: 0
```

In the foreground both transactions were applied as wanted: full. While the host was stopped, both waited, and the condition both waited on is the same: `host-started`. Waiting is not refusing; the transaction is not lost, and it is applied when the host is started again. The fulfillment of this lesson is therefore **pending**: the typical result of a fragment transaction is a wait that carries its condition by name.

**AC1.** The model reduces the host state to two values. The real platform manages the transaction queue separately among `created`, `started`, and `resumed`; this distinction was not measured.

## The Back Stack and the Back Button

`replace` discards the old fragment in the container. In a `replace` transaction that was not added to the back stack, the back button does not return to the old fragment: it leaves the activity itself. What the user expects is the previous screen.

```kotlin
// Main.kt — back stack and back button
fun goBack(previous: String, addedToBackStack: Boolean): Outcome =
    if (addedToBackStack) Outcome.Given(previous) else Outcome.Given("activity-exit")

fun main() {
    val asks = listOf(
        Ask("with-stack-back", "ListFragment", goBack("ListFragment", addedToBackStack = true)),
        Ask("without-stack-back", "ListFragment", goBack("ListFragment", addedToBackStack = false)),
    )
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
}
```

```text
with-stack-back: wanted=ListFragment | given=ListFragment -> full
without-stack-back: wanted=ListFragment | given=activity-exit -> substitute
full: 1
substitute: 1
pending: 0
unmet: 0
```

The transaction added to the back stack brought `ListFragment` back: full. A value also came for the transaction that was not added, but not the wanted one: `activity-exit`. The back button did something, but not what the user wanted; this is a substitute. The detail of the back stack, together with the back stack across activities, is taken up again in the last lesson of this course.

**AC2.** The model keeps a single previous fragment. The real back stack carries a list, and how many transactions are undone at once depends on the `popBackStack` call.

## Separate Lifetimes of a Fragment and Its View

A fragment has two lifetimes, and they do not end at the same moment. The fragment itself, that is the object, keeps living while it waits on the back stack. The view of the fragment, however, is destroyed with `onDestroyView` the moment the fragment goes onto the back stack, and is rebuilt with `onCreateView` when the user returns. For this reason every reference you hold to the view goes stale while the fragment is on the back stack: the object exists, but the view it points to no longer does.

This difference decides between which callbacks the code in a fragment that reaches the UI is safe. Access to the view lies between `onCreateView` and `onDestroyView`. An observer attached to the fragment should be bound to the lifetime of the view.

```kotlin
// Platform code: binds the observer to the view lifetime, not the fragment
import androidx.lifecycle.Observer

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    viewModel.items.observe(viewLifecycleOwner, Observer { items ->
        showItems(items)
    })
}
```

In the measurement the wanted value is always "update the UI". If the fragment has not yet built a view, the request waits for `onCreateView`. If the view is built, the request is fulfilled in full. While the fragment waits on the back stack there is no view; the request stays unmet and the reason is `no-view`.

```kotlin
// Main.kt — lifetime of the fragment view
fun updateView(stage: String): Outcome = when (stage) {
    "added" -> Outcome.Waiting("onCreateView")
    "view-created" -> Outcome.Given("updated")
    "on-back-stack" -> Outcome.Refused("no-view")
    else -> Outcome.Refused("no-fragment")
}

fun main() {
    val asks = listOf("added", "view-created", "on-back-stack", "removed").map {
        Ask("stage-$it", "updated", updateView(it))
    }
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
}
```

```text
stage-added: wanted=updated | waiting-on=onCreateView -> pending
stage-view-created: wanted=updated | given=updated -> full
stage-on-back-stack: wanted=updated | reason=no-view -> unmet
stage-removed: wanted=updated | reason=no-fragment -> unmet
full: 1
substitute: 0
pending: 1
unmet: 2
```

The two unmet lines reach the same result by different paths. In `no-fragment` the object does not exist either; the code is not even expected to hold a reference. In `no-view` the object lives and the reference looks valid, but the view it points to is gone. The second is more dangerous, because the error appears only at run time. The fix is to reset the view reference in `onDestroyView` and to bind observers to the lifecycle owner of the view.

**AC3.** The model separates the four stages by name. The order of transitions between the stages, intermediate callbacks such as `onViewStateRestored`, and how the view reference is reset were not measured.

## Arguments and Recreation

The platform can recreate a fragment: on a configuration change and after process death. When it recreates the fragment, it passes no parameter to the constructor. The default factory calls the parameterless constructor and then attaches to the fragment the `arguments` bundle that was given when it was created. A value given as a constructor parameter, if it is not in this bundle, is lost on recreation.

```kotlin
// Platform code: carries the value in the arguments bundle, not the constructor
class ListFragment : Fragment() {
    companion object {
        fun newInstance(category: String) = ListFragment().apply {
            arguments = bundleOf("category" to category)
        }
    }
}
```

In the measurement the wanted category is `book`. The value was given to the fragment by three different ways: with the bundle, with a constructor parameter, and through a class that has no parameterless constructor.

```kotlin
// Main.kt — path of the value when a fragment is recreated
fun recreate(way: String): Outcome = when (way) {
    "arguments" -> Outcome.Given("book")
    "constructor-parameter" -> Outcome.Given("all")
    else -> Outcome.Refused("no-empty-constructor")
}

fun main() {
    val asks = listOf("arguments", "constructor-parameter", "constructor-parameterized-only").map {
        Ask("way-$it", "book", recreate(it))
    }
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
}
```

```text
way-arguments: wanted=book | given=book -> full
way-constructor-parameter: wanted=book | given=all -> substitute
way-constructor-parameterized-only: wanted=book | reason=no-empty-constructor -> unmet
full: 1
substitute: 1
pending: 0
unmet: 1
```

The value given with the bundle survived the recreation: full. The value given with a constructor parameter works at first creation but falls back to the default on recreation; `all` is a value, not the wanted one: substitute. In the third way the recreation never starts, because the platform cannot find a parameterless constructor. The substitute is the most insidious result here: the app does not crash, it only shows the wrong category.

**AC4.** The model fixes the loss of the value as a fall back to the default. The real behavior depends on what the constructor does with the value, and when a custom fragment factory is used a parameterized constructor can work too; this was not measured.

## The Bounding Measurement

While the host was stopped, the transaction waited. But what if the host has already saved its state? After `onSaveInstanceState` is called, the platform counts the state as packed; adding a new transaction on top of the packed state means the transaction is lost when the process ends and the activity is rebuilt. For this reason the platform refuses `commit` at this stage. `commitAllowingStateLoss`, on the other hand, accepts the loss and applies the transaction, but the state of the transaction is not packed.

The same transaction can therefore receive three different fulfillments.

```kotlin
// Main.kt — the same transaction, three fulfillments after the state is saved
fun commitAfterSave(op: String, stateSaved: Boolean, allowLoss: Boolean): Outcome = when {
    !stateSaved -> Outcome.Given(op)
    allowLoss -> Outcome.Given("$op-stateless")
    else -> Outcome.Refused("state-saved")
}

fun main() {
    val op = "add:ListFragment"
    val asks = listOf(
        Ask("unsaved", op, commitAfterSave(op, stateSaved = false, allowLoss = false)),
        Ask("saved-commit", op, commitAfterSave(op, stateSaved = true, allowLoss = false)),
        Ask("saved-lossy", op, commitAfterSave(op, stateSaved = true, allowLoss = true)),
    )
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
}
```

```text
unsaved: wanted=add:ListFragment | given=add:ListFragment -> full
saved-commit: wanted=add:ListFragment | reason=state-saved -> unmet
saved-lossy: wanted=add:ListFragment | given=add:ListFragment-stateless -> substitute
full: 1
substitute: 1
pending: 0
unmet: 1
```

While the state was not saved, the transaction was applied in full. After it was saved, `commit` stayed unmet; the reason is `state-saved`. `commitAllowingStateLoss` gave a value, but not the transaction itself, only its stateless form: substitute. Three lines are three different fulfillments, and the transaction is the same.

These lines also draw the boundary between pending and unmet. While the host is stopped, the platform knows the transaction can be applied later and holds it. After the state is saved, there is no condition for the transaction to wait on: the condition has passed, and the transaction can no longer be applied safely.

**AC5.** The model does not throw the exception of the real platform; it returns the refusal as `Refused`. In the platform, `commit` ends with an exception in this case.

## Summary

- This lesson's criterion: **fulfillment**. A fragment transaction is fulfilled according to the state of the host activity. This lesson's fulfillment: **pending**.
- With the host in the foreground the transactions were fulfilled in full, and while it was stopped they were pending: 2 full and 2 pending.
- For a transaction not added to the back stack, the back button leaves the activity; another value comes in place of the wanted fragment: substitute.
- After the state is saved, `commit` stays unmet and `commitAllowingStateLoss` stays substitute; the same transaction receives three fulfillments.
- Pending is a request whose condition can be met in the future; unmet is a request whose condition has passed.

## Next Step

Fragments are UI sections; the work that runs in the background has no UI. The next lesson, Services, measures the app component without a UI and the rule the platform sets for a service started from the background.
