Lesson 11 / 15
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.
Published Updated
In this lesson
- 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
Contents
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.
// 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.
<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.
// 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.
// 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.
// 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)
}
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.
// 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)
}
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.
<!-- 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.
// 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)
}
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.
// 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)
}
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.
// 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)
}
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
resin 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.