---
title: 'The Resource System'
source: 'https://academia.sh/en/courses/android-platform-and-tools/resource-system'
course: 'Android Platform and Tools'
language: en
updated: '2026-10-21T05:02:36+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains the resource types and how a resource ID is used from code'
    - 'Measures that an exact match in a qualified directory is full and falling back to the default directory is substitute'
    - 'Shows that a resource missing from the default directory as well stays unmet'
    - 'Verifies with the bounding measurement that precedence decides the result when two qualifiers collide'
---

# The Resource System

A model of selecting a resource ID from qualified directories by configuration, covering exact match, falling back to the default, and qualifier precedence.

The previous lesson measured the merging of the manifest file, counting each element as a separate request. The manifest file declares what the app is; the text, images and layouts shown on screen live in resources. Resources are not embedded in code but kept in separate files, because the same resource must come from a different file depending on the device's language, screen size and day or night appearance. This lesson measures what the resource system gives when it selects a resource for the requested configuration.

The measurement axis of the course does not change: how does the platform fulfill this request. There are four fulfillments: full, substitute, pending and unmet. The rule, not a label, gives the fulfillment: a given value that equals the wanted one is full, and one that differs is substitute; pending and unmet come directly from the outcome. The file below carries that rule and is repeated verbatim in every lesson. The count of 3/2/2/1 from the opening lesson is not changed here.

```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 }}")
    }
}
```

## Resource Types and the Resource ID

Resources live under the `res` directory, in subdirectories separated by type. Text is in XML files in the `values` directory; drawable images are in the `drawable` directory; screen layouts are in the `layout` directory. Every resource has a name, and the build task produces a resource ID from these names: code reaches them by writing `R.string.title` or `R.drawable.logo`. The code does not know which file it will be read from, only the ID; choosing the file is the system's job.

```xml
<resources>
    <string name="title">Notes</string>
    <string name="subtitle">Everything in one place</string>
</resources>
```

This fragment is the text file in the default `values` directory. For German, a separate file with the same names can be placed in a `values-de` directory, containing only the `title` ID. The `-de` part of the directory name is called a qualifier. The fragment below shows reading by ID from code.

```kotlin
// Platform code: reading text by ID
val heading = getString(R.string.title)
```

This is platform code and is not run. On this line the system selects the directory that matches the device's current configuration; the model builds this rule of the platform. Qualifiers name a configuration property such as language, region, night appearance and smallest screen width: `values-de`, `values-de-rAT`, `values-night`, `layout-sw600dp`.

## The Selection Model

The model knows five things: the resource key, the wanted directory name, the device's configuration and the directories. The wanted directory is the one the configuration names exactly. A directory is a candidate if it contains the key and all of its qualifiers are in the configuration. Among the candidates, the one with the highest-precedence qualifier is selected; the order of precedence is language, region, screen width, night appearance. The default directory has no qualifier, so if it contains the key it is always a candidate. If there is no candidate, the resource cannot be found.

```kotlin
// Resources.kt — a model of resource selection
class ResourceDir(val name: String, val keys: Set<String>) {
    val tokens: List<String> = name.split("-").drop(1)
}

fun qualifierWeight(token: String): Int = when {
    token == "night" -> 1
    token.startsWith("sw") -> 2
    token.length == 3 && token.startsWith("r") -> 4
    else -> 8
}

fun resolveResource(key: String, wanted: String, config: Set<String>, dirs: List<ResourceDir>): Ask {
    val candidates = dirs.filter { key in it.keys && it.tokens.all { token -> token in config } }
    val best = candidates.maxByOrNull { dir -> dir.tokens.sumOf { qualifierWeight(it) } }
    val outcome = if (best == null) Outcome.Refused("no-default") else Outcome.Given(best.name)
    return Ask(key, wanted, outcome)
}
```

The weights build the order of precedence: language weighs eight, region four, width two and night one. The weight of a higher qualifier is greater than the sum of those below it; so language comes first no matter how many width and night qualifiers there are. The first measurement asks for three resources. The text key is in the German directory and the configuration is German. The image is in the night directory and the configuration includes night. The subtitle text, however, is found only in the default directory; the German directory does not contain it.

```kotlin
// Main.kt — exact match and falling back to the default
fun main() {
    val dirs = listOf(
        ResourceDir("values", setOf("string/title", "string/subtitle")),
        ResourceDir("values-de", setOf("string/title")),
        ResourceDir("drawable", setOf("drawable/logo")),
        ResourceDir("drawable-night", setOf("drawable/logo")),
    )
    val config = setOf("de", "night")
    val asks = listOf(
        resolveResource("string/title", "values-de", config, dirs),
        resolveResource("drawable/logo", "drawable-night", config, dirs),
        resolveResource("string/subtitle", "values-de", config, dirs),
    )
    for (ask in asks) ask.report()
    tally(asks)
}
```

```text
string/title: wanted=values-de | given=values-de -> full
drawable/logo: wanted=drawable-night | given=drawable-night -> full
string/subtitle: wanted=values-de | given=values -> substitute
full: 2
substitute: 1
pending: 0
unmet: 0
```

The first two IDs came from the exactly matching directory. For the subtitle the wanted directory was German but it was not there; the system gave the text in the default directory. That is a substitute, and the app did not crash: the default directory is a safety net.

**PS1.** The model knows four kinds of qualifier and expects at most one of each kind in a directory. The real set of qualifiers is much wider, and each kind has its own matching rule.

**PS2.** The model counts the presence of a qualifier in the configuration as sufficient. For numeric qualifiers such as screen width, the real system compares against a threshold, not equality; in this measurement `sw600dp` is treated only as a name.

## Configuration Change

When the device is rotated or night appearance is turned on, the configuration changes and the activity is recreated; resources are selected again for the new configuration. The measurement below asks about the same key in two configurations. The directories are `values`, `values-de` and `values-night`. In the German configuration `values-de` is selected. When night is added, both `values-de` and `values-night` become candidates; the wanted one, however, is `values-de-night`, which names the combination of the two. The third line asks, in a French configuration, for a key found only in the German directory.

```kotlin
// Main.kt — selecting again when the configuration changes
fun main() {
    val dirs = listOf(
        ResourceDir("values", setOf("string/title")),
        ResourceDir("values-de", setOf("string/title", "string/promo")),
        ResourceDir("values-night", setOf("string/title")),
    )
    val asks = listOf(
        resolveResource("string/title", "values-de", setOf("de"), dirs),
        resolveResource("string/title", "values-de-night", setOf("de", "night"), dirs),
        resolveResource("string/promo", "values-fr", setOf("fr"), dirs),
    )
    for (ask in asks) ask.report()
    tally(asks)
}
```

```text
string/title: wanted=values-de | given=values-de -> full
string/title: wanted=values-de-night | given=values-de -> substitute
string/promo: wanted=values-fr | reason=no-default -> unmet
full: 1
substitute: 1
pending: 0
unmet: 1
```

The same ID went from full to substitute when the configuration changed: the combined directory that the second configuration named did not exist. The third ID is unmet, because `string/promo` was only in the German directory and was not in the default directory; no candidate remained in the French configuration. What exactly the real system does in this case is not claimed in this lesson; the model only classifies that a counterpart was not given.

The configuration is an input to the measurement, but it is not the input that decides the result. The same configuration gives full for one ID and substitute for another. What decides is whether the given directory equals the wanted one.

## Type Directories and Screen Width

Resource directories are named by type. `values` carries simple values such as text, color and dimension, `drawable` carries images, and `layout` carries screen layouts. The directory of each type can take its own qualifiers. The screen-width qualifier is written like `sw600dp` and applies on devices whose smallest side is not less than that value. The fragment below shows a separate layout directory for wide screens.

```xml
<!-- layout-sw600dp/main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
```

The same name can also exist in the `layout` directory; on a wide screen the one in `layout-sw600dp` is selected, and on a narrow screen the one in the default directory. The wanted one is again the directory that the configuration names exactly. In the measurement the configuration is wide like a tablet, and German. It asks for three types: a layout, a text and an image. The layout is only in the wide-screen directory and in the default. The text is only in the German directory and in the default. The image is only in the default directory.

```kotlin
// Main.kt — resource types
fun main() {
    val dirs = listOf(
        ResourceDir("layout", setOf("layout/main")),
        ResourceDir("layout-sw600dp", setOf("layout/main")),
        ResourceDir("values", setOf("string/title")),
        ResourceDir("values-de", setOf("string/title")),
        ResourceDir("drawable", setOf("drawable/logo")),
    )
    val config = setOf("de", "sw600dp")
    val asks = listOf(
        resolveResource("layout/main", "layout-sw600dp", config, dirs),
        resolveResource("string/title", "values-de", config, dirs),
        resolveResource("drawable/logo", "drawable-sw600dp", config, dirs),
    )
    for (ask in asks) ask.report()
    tally(asks)
}
```

```text
layout/main: wanted=layout-sw600dp | given=layout-sw600dp -> full
string/title: wanted=values-de | given=values-de -> full
drawable/logo: wanted=drawable-sw600dp | given=drawable -> substitute
full: 2
substitute: 1
pending: 0
unmet: 0
```

The first two requests were fulfilled from the directory the configuration named. The third asked for an image specific to wide screens, but only the default image existed; the default was given, and it was a substitute.

**PS3.** The model recognizes only the `de`, `rAT`, `night` and `sw` qualifiers. In the real system there are other qualifiers such as density, orientation and platform version; the measurement does not cover them.

## The Steps of Elimination

Selection is done in two steps. First, the directories that conflict with the configuration are eliminated: a directory with a qualifier that is not in the configuration cannot be a candidate. Then, among the remaining candidates, the best one is selected by the qualifier order of precedence. In the first step `values-night` is eliminated in a daytime configuration, while `values-de` stays in a German configuration. If the candidate list is empty, the resource cannot be found. If it is not in the default directory either, that means the resource is not defined anywhere. In the measurement the configuration is daytime and German. There is a night directory and a German directory; in addition, a key that is in no directory is asked for.

```kotlin
// Main.kt — an eliminated directory and a missing key
fun main() {
    val dirs = listOf(
        ResourceDir("values", setOf("string/title")),
        ResourceDir("values-night", setOf("string/title")),
        ResourceDir("values-de", setOf("string/title")),
    )
    val config = setOf("de")
    val asks = listOf(
        resolveResource("string/title", "values-night", config, dirs),
        resolveResource("string/title", "values-de", config, dirs),
        resolveResource("string/missing", "values", config, dirs),
    )
    for (ask in asks) ask.report()
    tally(asks)
}
```

```text
string/title: wanted=values-night | given=values-de -> substitute
string/title: wanted=values-de | given=values-de -> full
string/missing: wanted=values | reason=no-default -> unmet
full: 1
substitute: 1
pending: 0
unmet: 1
```

Although the night directory was wanted, it was eliminated in the daytime configuration and the German directory was given: substitute. The key that is in no directory stayed unmet; the reason is named `no-default`, because the model could not find it in the default directory either.

**PS4.** The model eliminates by checking whether the qualifier is in the configuration. The real system proceeds by eliminating qualifiers one by one in order of precedence; in most cases the two give the same result, but the measurement does not compare them.

## The Bounding Measurement

The model matches qualifiers one by one; it does not know that one qualifier may overlap with a close neighbor. A separate directory such as `values-de-rAT` can be written for every region where German is spoken. If there is no region directory and only `values-de` exists, an Austrian-region configuration reads from the German directory: the model classifies this as a substitute. The second and third lines test the collision of two qualifiers. The directories are `values-de` and `values-night`, and the configuration contains both. It is asked first with both present, then after the German directory is removed.

```kotlin
// Main.kt — region and precedence
fun main() {
    val region = listOf(
        ResourceDir("values", setOf("string/title")),
        ResourceDir("values-de", setOf("string/title")),
    )
    val both = listOf(
        ResourceDir("values-de", setOf("string/title")),
        ResourceDir("values-night", setOf("string/title")),
        ResourceDir("values", setOf("string/title")),
    )
    val nightOnly = both.filter { it.name != "values-de" }
    val config = setOf("de", "rAT", "night")
    val asks = listOf(
        resolveResource("string/title", "values-de-rAT", config, region),
        resolveResource("string/title", "values-de-night", config, both),
        resolveResource("string/title", "values-de-night", config, nightOnly),
    )
    for (ask in asks) ask.report()
    tally(asks)
}
```

```text
string/title: wanted=values-de-rAT | given=values-de -> substitute
string/title: wanted=values-de-night | given=values-de -> substitute
string/title: wanted=values-de-night | given=values-night -> substitute
full: 0
substitute: 3
pending: 0
unmet: 0
```

All three lines are substitute, and the rule does not tell them apart: in all three the given differs from the wanted. What separates them is the given column. In the second line, with two candidates, language won. In the third line, since the language directory was gone, the night directory was selected. The model builds the order of precedence; the rule sees only the result, not why that result was selected. The tally counts three substitutes and does not show that precedence worked.

**PS5.** The model builds precedence as language, region, width, night; these four qualifiers are in this order in the platform's list as well. The platform's full precedence list is longer, and it does not use a sum of weights but eliminates qualifiers in order. The measurement shows only that the order works for these four qualifiers.

## Summary

- This lesson's criterion: **fulfillment**; whether the given directory equals the wanted directory, not the configuration itself. This lesson's fulfillment: **substitute**.
- Resources live under `res` in directories by type; code reaches them by resource ID, and the system selects the file.
- Qualifiers are configuration properties at the end of the directory name: language, region, night appearance, screen width.
- If the directory the configuration names exists, the resource is full; a resource coming from the default directory is substitute.
- A resource that is not in the default directory either is unmet.
- When two qualifiers collide, the order of precedence decides the result; the rule does not see this reason, only the equality of the given and the wanted.

## Next Step

Resources and the manifest file must be built to come together into a runnable app. The next lesson covers the build system and measures which version is given when dependency versions collide.
