Resources, Configuration, Localization and Themes
|
This content was generated with the assistance of AI and should be verified against the official documentation of Android before being relied on in production. See the section bibliography for the reference material consulted while preparing these pages. |
Every Android app ships a second, declarative half alongside its Kotlin code: the res/ tree. This page covers
how that tree is organized, how a single logical resource (a string, a color, an icon) can have many alternative
values selected automatically for the current device, how both the classic View system and Jetpack Compose read
those values at runtime, and what happens to an app’s UI when the configuration that selected them changes while
the app is running. It targets the section’s baseline — Android 17 (API 37), Android Studio Quail,
AGP 9.4.1, Kotlin 2.4.20 and Compose BOM 2026.09.00 — and uses TrailMate, the running example
introduced in Getting Started with Android, for every snippet. TrailMate
lists hiking trails with a name, region, length, difficulty and an optional photo; its resources below back both
its classic Views screens and its Compose screens.
This page does not re-teach Kotlin syntax — see the
Kotlin on Android page of the
Kotlin Reference for language-level Android interop such as KTX
extension functions. It also does not cover how a ConstraintLayout or a Compose Column is built from these
resources; that belongs to Views, XML Layouts, View Binding and Material Components and Jetpack Compose Fundamentals. What happens to state
that a screen was holding across the configuration change described here — as opposed to which resource
values get re-selected — is covered in full, with rememberSaveable, onSaveInstanceState and
SavedStateHandle, by Saving UI State: Views vs. Compose, later in this section; this page only explains why that recreation happens and how to control it.
The res/ Directory and Resource Types
Every module has a src/main/res/ directory (plus src/debug/res/, src/release/res/, per-flavor
src/<flavor>/res/, and so on, merged at build time as described in
Project Structure, Gradle and Build Variants). Inside it,
resources are grouped into subdirectories by type, not by feature: every string lives under values/
regardless of which screen uses it, every drawable under drawable/, and so on. A representative slice of
TrailMate’s res/ tree looks like this:
app/src/main/res/
values/
strings.xml
plurals.xml
dimens.xml
colors.xml
themes.xml
values-night/
colors.xml
values-es/
strings.xml
values-w600dp/
dimens.xml
drawable/
ic_trail_marker.xml
ic_favorite_filled.xml
mipmap-anydpi-v26/
ic_launcher.xml
ic_launcher_round.xml
mipmap-hdpi/
ic_launcher_foreground.png
font/
inter_regular.ttf
inter_semibold.ttf
raw/
trail_complete_chime.mp3
xml/
data_extraction_rules.xml
network_security_config.xml
Each top-level directory corresponds to one resource type recognized by the Android Gradle Plugin’s resource compiler (AAPT2):
-
values/holds resources defined as XML elements rather than files of their own: strings, quantity strings (plurals), dimensions, colors, styles/themes, arrays, integers and booleans. All of these can be split across as many files as convenient insidevalues/—strings.xml,plurals.xmlanddimens.xmlabove are three separate files merged into one resource table; the file name has no meaning to the build, only the XML element names andnameattributes do. -
drawable/holds anything that can be drawn: vector drawables (<vector>XML, preferred for icons and simple art since they scale to any density without extra files), state lists, shape drawables, and raster images (PNG/WebP/JPEG) when a vector is not appropriate (photos, complex illustrations). -
mipmap/is specifically for launcher icons. Unlikedrawable/, mipmap resources for a given density are kept even when the app is shown at a different density (for example on a home-screen launcher that deliberately renders icons larger than the current display density would otherwise select), which is why launcher icons get their own resource type instead of living indrawable/. Since API 26, adaptive icons are an XML mipmap resource (mipmap-anydpi-v26/ic_launcher.xml) that references a separate foreground and background layer, letting the launcher mask, animate and theme the icon consistently across device skins. -
font/holds font files (.ttf/.otf) or XML font families, compiled as resources and referenced like any other resource (R.font.inter_regular); TrailMate’sTypographyin Material 3 and Theming in Compose is built from fonts declared here. Fonts can also be fetched at runtime from Google Fonts via the Downloadable Fonts API instead of bundled, trading APK size for a network dependency at first use. -
raw/holds arbitrary files copied byte-for-byte into the APK with no processing — audio clips, JSON fixtures, licenses — opened withresources.openRawResource(R.raw.trail_complete_chime). -
xml/holds miscellaneous XML configuration consumed by the framework rather than the app’s own code, such asnetwork_security_config.xml(covered in Networking with Retrofit, OkHttp and Ktor) anddata_extraction_rules.xmlfor Auto Backup. -
layout/,menu/,anim/,animator/,transition/,interpolator/hold their respective XML definitions and are covered where they are used: layouts in Views, XML Layouts, View Binding and Material Components, the rest in Custom Views, Touch, Drawing and View Animation.
A typical values/strings.xml and plurals.xml for TrailMate:
<!-- app/src/main/res/values/strings.xml -->
<resources>
<string name="app_name">TrailMate</string>
<string name="trail_list_title">Trails near you</string>
<string name="trail_detail_length_km">%1$.1f km</string>
<string name="cd_favorite_toggle">Toggle favorite</string>
</resources>
<!-- app/src/main/res/values/plurals.xml -->
<resources>
<plurals name="trail_count">
<item quantity="one">%d trail</item>
<item quantity="other">%d trails</item>
</plurals>
</resources>
A <plurals> resource — a quantity string — picks one of several wordings (zero, one, two, few,
many, other) based on a count and the current language’s own pluralization rules, which is why the two
English buckets above (one/other) become more of them once a values-pl/plurals.xml or values-ar/
plurals.xml alternative is added for a language with a richer plural system; English never uses zero, two,
few or many, but the resource system evaluates whichever buckets that language defines. See
Android Developers — String
Resources: Quantity Strings (Plurals).
dimens.xml and colors.xml follow the same shape:
<!-- app/src/main/res/values/dimens.xml -->
<resources>
<dimen name="trail_card_corner_radius">12dp</dimen>
<dimen name="trail_card_padding">16dp</dimen>
</resources>
<!-- app/src/main/res/values/colors.xml -->
<resources>
<color name="trail_easy">#2E7D32</color>
<color name="trail_moderate">#F9A825</color>
<color name="trail_hard">#C62828</color>
</resources>
See Android Developers — More
Resource Types: Dimension and
Android Developers — More Resource
Types: Color. In a project that also uses Compose, these colors.xml/themes.xml values typically only back
the small amount of View-based UI that remains (or the launcher theme/splash screen); the Compose ColorScheme
in TrailMateTheme is defined in Kotlin instead, as shown in Material 3 and Theming in Compose.
For a general overview of how these directories are grouped, named and merged, see Android Developers — Providing Resources.
The R Class, and Reading Resources from Views and Compose
At build time, AAPT2 assigns every resource a stable integer id and generates a R class (and, for a library
module, an equivalent R class per consuming module) with one nested class per resource type:
R.string.trail_list_title, R.drawable.ic_trail_marker, R.dimen.trail_card_padding,
R.mipmap.ic_launcher, R.font.inter_regular, R.raw.trail_complete_chime. Compose modules generate an
additional Res object under Compose Multiplatform’s own resource system, but for a plain Android app — as
TrailMate is here — both the Views code and the Compose code read resources through the same generated
R class; Compose simply wraps the lookup in composable helper functions.
From Views and other non-composable code, resources are read through a Context (or, for layout XML,
directly by reference):
// feature/trails/src/main/kotlin/com/example/trailmate/feature/trails/TrailListFragment.kt
val title = requireContext().getString(R.string.trail_list_title)
val count = resources.getQuantityString(R.plurals.trail_count, trailCount, trailCount)
val padding = resources.getDimensionPixelSize(R.dimen.trail_card_padding)
val marker = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_trail_marker)
See Android Developers — Resources and
Android Developers — AppCompatResources (the AppCompat-aware drawable loader, needed for vector drawables on API levels below 21
without the compatibility flag).
From a @Composable function, the equivalent lookups are top-level Compose functions that take the
resource id, resolve the current Configuration from LocalConfiguration/LocalContext internally, and — critically — register the composable to recompose if that configuration changes while it is on screen (a plain
context.getString(…) call inside a composable would read the resource once and never update it if the
device configuration changes without a full activity recreation, such as a font-scale-only change on newer
Android versions):
// feature/trails/src/main/kotlin/com/example/trailmate/feature/trails/TrailListScreen.kt
@Composable
fun TrailListItem(trail: Trail, count: Int) {
Text(text = stringResource(R.string.trail_list_title))
Text(text = pluralStringResource(R.plurals.trail_count, count, count))
Image(
painter = painterResource(R.drawable.ic_trail_marker),
contentDescription = stringResource(R.string.cd_favorite_toggle)
)
}
stringResource accepts vararg formatArgs for String.format-style placeholders (used above for
trail_detail_length_km); pluralStringResource takes both the raw count: Int used to pick the plural bucket
and the format arguments passed to that bucket’s template (often the same Int, as above).
painterResource returns a Painter for a drawable resource — a vector drawable, a raster image, or (since it
dispatches on the resource’s own type) even an XML shape drawable — and is the Compose equivalent of
AppCompatResources.getDrawable. See
Android Developers — Resources in Compose and the
stringResource/pluralStringResource/painterResource entries in the
Compose androidx.compose.ui.res
package reference.
Configuration Qualifiers and the Best-Match Algorithm
The directory names above (values-night, values-es, values-w600dp) are alternative resource directories:
the base directory name (values, drawable, mipmap) suffixed with one or more dash-separated qualifiers,
each describing a condition on the device’s current Configuration under which that whole directory’s
resources should be preferred over the base ones. The most common qualifiers, listed in the order Android
requires them to appear when more than one is combined on the same directory name:
| Qualifier example | Matches | Typical use |
|---|---|---|
|
Language (and optional region/script) |
Localized strings, plurals, arrays |
|
Smallest-width class (the shorter of width/height in dp, orientation-independent) |
Tablet vs phone layout selection |
|
Current available width/height in dp |
Layouts that should react to multi-window/ resizing, not just device class |
|
Screen orientation |
Orientation-specific layouts |
|
UI night mode ( |
Dark-theme colors, dark-tinted illustrations |
|
Screen density bucket |
Density-specific raster drawables/mipmaps |
|
Minimum API level |
Resources only valid from that platform version onward (adaptive icons at
|
Combining qualifiers out of that order is a build error: a large-screen landscape directory must be named
values-sw600dp-land/, and AAPT2 rejects values-land-sw600dp/.
At startup, and again on every configuration change, the framework’s resource system walks every alternative
directory for a given base type and eliminates any directory whose qualifier does not match the current
configuration at all (a values-fr/ directory is dropped outright on a device set to Spanish). Among the
directories that remain, it picks the best match one qualifier at a time, in a fixed precedence order (MCC/MNC,
language/region, layout direction, smallest width, available width, available height, screen size, screen
aspect, round screen, wide color gamut, high dynamic range, orientation, UI mode, night mode, density, touchscreen,
keyboard, navigation, platform version) — the first qualifier in that order on which two candidate directories
differ decides the match, and every later qualifier is irrelevant to that comparison. This is why, for example, a
density qualifier can never override a language mismatch: language is resolved long before density in the
precedence list. If no alternative directory matches a given qualifier axis at all, the base (unqualified)
directory’s value is used, which is why every resource should also exist, without any qualifier, as a sensible
default. See
Android Developers — Providing Resources: Alternative Resources for the full qualifier table and precedence order.
TrailMate uses this mechanism, rather than runtime if checks, for exactly the three axes shown in the tree
above: values-night/colors.xml swaps trail_easy/trail_moderate/trail_hard for dark-theme-appropriate
tints, values-es/strings.xml supplies Spanish strings, and values-w600dp/dimens.xml widens
trail_card_padding for the two-pane tablet layout described in Adaptive Layouts, Edge-to-Edge and Large Screens. In Compose, the same qualifier-selected values are read with the ordinary stringResource/
dimensionResource/colorResource calls shown above — there is no separate Compose-only qualifier syntax.
Density-Independent Sizing: dp and sp
Because a "pixel" means a physically different size on a 160-dpi device and a 480-dpi device, Android layout dimensions are almost never expressed in raw pixels. Two density-independent units exist instead:
-
dp(density-independent pixels) is defined so that1dpis one physical pixel on a baseline 160-dpi ("mdpi") screen, and the framework scales it linearly by the device’s density bucket at layout time —16dprenders as roughly 24 raw pixels on a 240-dpi ("hdpi") screen. Every layout dimension — margins, padding, icon sizes,dimens.xmlvalues such astrail_card_paddingabove — is expressed indp(or, in Compose, theDptype returned by16.dp) so that the same layout occupies the same physical size across devices of different density. -
sp(scale-independent pixels) behaves likedpbut additionally scales with the user’s font size accessibility setting, so text grows independently of layout when a user increases font scale in system settings. Text sizes — in XMLtextSizeattributes, ComposeTextStyle.fontSize, and thespextension on numeric literals — always usesp, neverdp, precisely so that accessibility font scaling reaches text without also inflating unrelated layout spacing.
Density buckets (the ldpi…xxxhdpi qualifiers from the table above) exist because raster assets — PNGs,
mipmap launcher icons — cannot be generated at layout time the way a vector drawable or a dp measurement can;
a raster asset must be pre-rendered at each density it will be shown at, which is exactly what
mipmap-hdpi/ic_launcher_foreground.png and its mdpi/xhdpi/xxhdpi/xxxhdpi siblings in the tree above
provide. Preferring vector drawables for anything that is not a photograph avoids needing per-density raster
exports at all, since a <vector> resource is rendered to the correct pixel size for whatever density it is
displayed at. See Android Developers — Supporting Multiple Screens: Screen Densities for the exact dpi value of each bucket and the density-independent
pixel formula.
Localization, RTL and Per-App Languages
Every user-visible string, plural and content description should be a resource, never a literal embedded in
Kotlin or in layout XML, precisely so that a values-<locale>/ directory can override it without touching code.
TrailMate’s values-es/strings.xml above is one such override; adding support for another language is adding
another values-<locale>/strings.xml (and plurals.xml, if any plural rules differ) with the same resource
names and translated values — nothing in the Kotlin or Compose code changes. See
Android Developers — Localization and
Android Developers — Support
Different Languages and Cultures for locale-resolution details (including the b+language+region BCP 47 form
needed for scripts and regions a two-letter qualifier cannot express) and for pseudolocale testing.
Languages that read right-to-left (Arabic, Hebrew, Persian, Urdu) require layout mirroring, not just translated
strings: start/end edges swap, icons that imply direction (a "forward" chevron) should mirror, and text alignment
follows the script. The View system and Compose both default to mirroring automatically for any layout expressed
with start/end attributes and modifiers rather than left/right ones — paddingStart/paddingEnd (not
paddingLeft/paddingRight), Modifier.padding(start = …, end = …), TextAlign.Start, layoutDirection-aware
Arrangement in Compose rows — so writing layouts in terms of start/end throughout TrailMate, as the snippets
on this page already do, is what makes RTL support close to automatic rather than a retrofit. The manifest’s
android:supportsRtl="true" attribute (set by the default Android Studio templates) opts the whole app into
this mirroring; an icon that should not mirror (a photograph, a brand mark) is marked
android:autoMirrored="false" on its <vector> resource, which defaults to true.
Per-app language preferences let a user pick TrailMate’s display language independently of the device’s system language, from the app’s own Settings screen or — since API 33 — from the system’s per-app language settings screen, without TrailMate having to ship its own in-app locale-picker UI to persist and re-apply the choice on every launch. Two complementary mechanisms provide this:
-
res/xml/locales_config.xmllists the locales TrailMate ships translations for, referenced from the manifest application element viaandroid:localeConfig="@xml/locales_config". From API 33 onward, this alone is enough for the system’s per-app language screen to appear and to persist the user’s choice across app restarts and updates, with no extra code. -
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags("es")), called from TrailMate’s own in-app language picker (inSettingsScreen), does the same thing programmatically, is backward-compatible to API 21 through theandroidx.appcompatcompatibility layer, and is the call the system’s per-app language screen itself triggers under the hood on API 33+;AppCompatDelegate.getApplicationLocales()reads the current per-app selection back.
See Android Developers — Support Per-App
Language Preferences for the full setup, including how locales_config.xml and setApplicationLocales interact
and the Play Store’s automatic split-APK handling of per-locale resources.
Night Qualifiers and Dark Theme
The night/notnight qualifier pair selects resources for Android’s system-wide dark theme the same way any
other qualifier does: values-night/colors.xml above overrides trail_easy/trail_moderate/trail_hard with
tints that read well on a dark surface, and the base values/colors.xml (implicitly notnight) supplies the
light-theme values. Whether night or notnight matches follows the app’s own night-mode setting — system
default, always-light, or always-dark, chosen with AppCompatDelegate.setDefaultNightMode(…) and typically
exposed as a preference in SettingsScreen — layered on top of the system’s own dark-theme toggle. In Compose,
TrailMateTheme reads isSystemInDarkTheme() to choose between its light and dark ColorScheme, which Material 3 and Theming in Compose covers together with Android 12+ dynamic color; the
qualifier-based values-night/ resources on this page remain relevant for
whatever View-based chrome or launcher/splash-screen theming a Compose-first app still keeps in XML. See
Android Developers — Dark Theme.
Configuration Changes and Activity Recreation
A configuration change is any change to the device’s Configuration while the app is running: rotating the
device (orientation), entering/leaving multi-window or resizing a freeform/desktop window (screen size),
plugging in a keyboard, toggling dark theme, changing the system language, or changing the font-scale
accessibility setting. By default, the framework’s answer to a configuration change is blunt but simple: it
destroys and recreates the current Activity (onDestroy() then a fresh onCreate()), and every Fragment
attached to it, from scratch. This is precisely what makes the qualifier system on this page effective without
any code to react to it — when onCreate() runs again, resource lookups re-resolve against the new
configuration automatically, picking up values-land/, values-night/, values-w600dp/, or whichever
alternative directories now match, with no if (configuration changed) branch anywhere in TrailMate’s own code.
The cost is that anything the destroyed instance was holding only in memory — a var on the Activity or
Fragment, an unsaved scroll position, an in-progress text field — is lost unless it was explicitly retained
across the recreation. ViewModel instances survive a configuration change (they are kept by the Activity's
non-configuration retained store and handed to the new Activity instance, as ViewModel, Lifecycle-Aware Components, Coroutines and Flow explains), so state that already lives in a
ViewModel — TrailMate’s TrailListUiState, for example — survives without any extra work; UI-only state that a Composable or a View holds directly does not, and needs
rememberSaveable/onSaveInstanceState/SavedStateHandle to survive both a configuration change and the
stricter case of system-initiated process death. That mechanism, and a full Views-vs-Compose comparison of what
survives which event, is the subject of Saving UI State: Views vs. Compose later in this section — this page stops at why recreation happens.
It is tempting to suppress the recreation instead, by declaring
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode" on an <activity> in the
manifest: the Activity is then kept alive across that configuration change, and only
onConfigurationChanged(Configuration) is called, with resources re-resolved but the Activity never
recreated. This is discouraged as a general strategy for several reasons: it opts out of the automatic,
qualifier-driven resource re-resolution at the framework level for every UI element that isn’t manually
re-read inside onConfigurationChanged — a layout inflated once at onCreate() keeps whatever drawables and
dimensions it originally resolved unless the code explicitly re-inflates or re-reads them; it does nothing for
system-initiated process death, which is the scenario that most often catches an app off guard in production and
that only proper state saving (not configChanges) protects against; it must list every configuration axis the
Activity cares about, is easy to get subtly wrong (missing uiMode silently breaks dark-theme switching on that
screen, for instance), and diverges further from the platform’s default behavior with every Android release. The
recommended alternative is almost always to let recreation happen and to save the small amount of state that
actually needs to survive it, which tends to be less code overall than hand-rolling onConfigurationChanged
correctly for every relevant axis. See
Android Developers — Handle Configuration
Changes.
Resource Shrinking
Every string, drawable, color and layout that ends up unused by the final app — a leftover translation for a
removed feature, a drawable only referenced from code that R8 has since eliminated — still costs APK size if
left in the build output. AGP’s resource shrinker, enabled together with code shrinking via
isMinifyEnabled = true and isShrinkResources = true on a release buildType in build.gradle.kts, walks the
manifest, compiled code and any tools:keep/tools:discard hints in res/raw/keep.xml to find every resource
actually reachable, then removes the rest from the APK/AAB. It runs after R8’s code shrinking, in the same build
step, so that a resource only referenced from now-dead code is also caught. It can additionally remove
unused alternative-locale resource sets entirely via the resourceConfigurations (or the newer, string-array
based) mechanism in defaultConfig, when TrailMate ships translations for locales the Play Store’s own
per-device language split already makes redundant to keep unconditionally in the base artifact. See
Android Developers — Shrink, Obfuscate, and Optimize Your App
(the "Shrink resources" section covers isShrinkResources, tools:keep/tools:discard, and how it composes
with R8).
References
-
Android Developers — String Resources: Quantity Strings (Plurals)
-
Android Developers —
androidx.compose.ui.respackage reference -
Android Developers — Providing Resources: Alternative Resources
-
Android Developers — Supporting Multiple Screens: Screen Densities
-
Android Developers — Support Different Languages and Cultures
-
Android Developers — Shrink, Obfuscate, and Optimize Your App