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 inside values/ — strings.xml, plurals.xml and dimens.xml above are three separate files merged into one resource table; the file name has no meaning to the build, only the XML element names and name attributes 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. Unlike drawable/, 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 in drawable/. 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’s Typography in 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 with resources.openRawResource(R.raw.trail_complete_chime).

  • xml/ holds miscellaneous XML configuration consumed by the framework rather than the app’s own code, such as network_security_config.xml (covered in Networking with Retrofit, OkHttp and Ktor) and data_extraction_rules.xml for 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

es, fr, b+es+419 (BCP 47)

Language (and optional region/script)

Localized strings, plurals, arrays

sw600dp

Smallest-width class (the shorter of width/height in dp, orientation-independent)

Tablet vs phone layout selection

w600dp, h480dp

Current available width/height in dp

Layouts that should react to multi-window/ resizing, not just device class

land, port

Screen orientation

Orientation-specific layouts

night, notnight

UI night mode (UI_MODE_NIGHT_YES/NO)

Dark-theme colors, dark-tinted illustrations

ldpi…xxxhdpi

Screen density bucket

Density-specific raster drawables/mipmaps

v26, v33

Minimum API level

Resources only valid from that platform version onward (adaptive icons at v26, per-app-language resources at v33)

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 that 1dp is 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 — 16dp renders as roughly 24 raw pixels on a 240-dpi ("hdpi") screen. Every layout dimension — margins, padding, icon sizes, dimens.xml values such as trail_card_padding above — is expressed in dp (or, in Compose, the Dp type returned by 16.dp) so that the same layout occupies the same physical size across devices of different density.

  • sp (scale-independent pixels) behaves like dp but 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 XML textSize attributes, Compose TextStyle.fontSize, and the sp extension on numeric literals — always use sp, never dp, 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.xml lists the locales TrailMate ships translations for, referenced from the manifest application element via android: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 (in SettingsScreen), does the same thing programmatically, is backward-compatible to API 21 through the androidx.appcompat compatibility 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).