Resources: strings, drawables and configuration
Mandi Bhav's users are farmers and traders in Maharashtra, and many of them would rather read Marathi or Hindi than English. Look at the starter: the words "Namaste from" are typed inside a Kotlin string in MainActivity.kt. There is no way to translate that without shipping a different app. Android's answer is resources: everything that is not code, kept in res/ and looked up by name at run time, so the system can pick the right version for each phone. This lesson moves Mandi Bhav's text into resources and shows how Android chooses between versions.
strings.xml, and why hardcoded text breaks
The starter's res/values/strings.xml holds one string, the app name the manifest uses. Here it is with the text the first screen needs:
<resources>
<string name="app_name" translatable="false">Mandi Bhav</string>
<string name="greeting">Namaste from %1$s</string>
<string name="modal_price">Modal price: ₹%1$d per quintal</string>
<string name="market_and_commodity">%1$s at %2$s</string>
<plurals name="markets_found">
<item quantity="one">%d market found</item>
<item quantity="other">%d markets found</item>
</plurals>
</resources>
nameis how code refers to the string. The build generates a class calledRwith an integer id for every resource, so this becomesR.string.greeting.%1$sand%1$dare numbered placeholders: argument 1 as a string, argument 1 as an integer. The numbers matter, as you will see in Hindi.translatable="false"marks a brand name that stays the same in every language.
And the composable that uses them:
@Composable
fun PriceSummary(modifier: Modifier = Modifier) {
val marketCount = 3
Column(modifier = modifier.padding(dimensionResource(R.dimen.screen_padding))) {
Text(text = stringResource(R.string.greeting, stringResource(R.string.app_name)))
Text(text = stringResource(R.string.market_and_commodity, "Onion", "Lasalgaon"))
Text(
text = stringResource(R.string.modal_price, 1850),
color = colorResource(R.color.price_highlight),
)
Text(text = pluralStringResource(R.plurals.markets_found, marketCount, marketCount))
}
}
The imports are androidx.compose.ui.res.stringResource, pluralStringResource, colorResource and dimensionResource, plus R from your own package. On the emulator in English this showed:
Namaste from Mandi Bhav
Onion at Lasalgaon
Modal price: ₹1850 per quintal
3 markets found
stringResource reads the string for the phone's current language, fills in the arguments, and returns a String. It is a composable function, so it only works inside a composable; in an activity, getString(R.string.greeting, ...) does the same job, and only after onCreate has begun.
The rule: no user-visible text in Kotlin. A hardcoded string is a translation you can never ship and a typo you have to find with search. The cost is one more file to open for every label. That cost is real, and it is small next to the alternative.
A second language: values-hi
Create res/values-hi/strings.xml. The -hi is a qualifier: this folder applies when the phone's language is Hindi. It holds the same names with Hindi text:
<resources>
<string name="greeting">%1$s की ओर से नमस्ते</string>
<string name="modal_price">मॉडल भाव: ₹%1$d प्रति क्विंटल</string>
<string name="market_and_commodity">%2$s में %1$s</string>
<plurals name="markets_found">
<item quantity="one">%d मंडी मिली</item>
<item quantity="other">%d मंडियाँ मिलीं</item>
</plurals>
</resources>
No app_name, because it is not translatable. To see it, you can switch the whole emulator to Hindi in Settings, or switch only this app with one command (Android 13 and later):
adb shell cmd locale set-app-locales com.riztechacademy.mandibhav --user 0 --locales hi
and back to the phone's language with:
adb shell "cmd locale set-app-locales com.riztechacademy.mandibhav --user 0 --locales ''"
With the app in Hindi, the same code, untouched and not rebuilt, showed the Devanagari text: the greeting with "Mandi Bhav" first, the modal price in Hindi with ₹1850, and the Hindi plural for three markets.
Two details from that screen are worth a second look.
The arguments swapped places. English says "Onion at Lasalgaon"; Hindi puts the place first, "Lasalgaon में Onion". Because the placeholders are numbered, the Hindi string writes %2$s before %1$s and the Kotlin call does not change. With unnumbered %s %s, every translator would need a programmer.
"Onion" stayed in English. Resources translate the text you put in resources. "Onion" and "Lasalgaon" were passed in from Kotlin as data, so they arrive as they are. When Mandi Bhav's prices come from a server, commodity names come with them, and translating those is a data problem, not a resources one.
Plurals
pluralStringResource(R.plurals.markets_found, marketCount, marketCount)
The count appears twice: the first chooses which <item> to use, the second fills in %d. They are usually the same number and occasionally not ("1 of 3 markets").
The trap plurals exist to prevent is this:
val label = if (count == 1) "$count market found" else "$count markets found"
It is correct for English and wrong for Mandi Bhav's second language. Set marketCount to 0 and run in both languages: English shows "0 markets found", using the other form, but Hindi chose the one form, the same as for a single market, because Hindi's plural rules treat zero like one. The if above would have given Hindi the wrong form, and no amount of testing in English would have shown it. Other languages have more forms still, with categories such as few and many. A <plurals> resource lets each translation define the forms its own language needs, and Android picks the right one from the count using that language's rules. Lint knows the pattern too: it flagged Modal price: ₹%1$d per quintal as a PluralsCandidate because a number is followed by a word. Here that warning is a false positive, because a price is not a count of quintals, and you can say so with tools:ignore.
The trap: a missing translation
Add res/values-mr/strings.xml for Marathi with only the greeting in it, and run ./gradlew lintDebug:
strings.xml:4: Error: "modal_price" is not translated in "mr" (Marathi) [MissingTranslation]
<string name="modal_price">Modal price: ₹%1$d per quintal</string>
~~~~~~~~~~~~~~~~~~
strings.xml:5: Error: "market_and_commodity" is not translated in "mr" (Marathi) [MissingTranslation]
strings.xml:6: Error: "markets_found" is not translated in "mr" (Marathi) [MissingTranslation]
The app still builds and runs; a Marathi phone shows the greeting in Marathi and everything else in English, falling back to values/. That fallback is what saves you, and it is also what hides the gap: nobody on the team reads Marathi, so nobody notices. Lint is the only thing that will. Run it before every release.
Notice that lintDebug failed the build: MissingTranslation is an error, not a warning. That is deliberate, and it is why lint belongs in your release routine.
The fallback only works one way. Every resource must exist in the unqualified folder. Put a string only in values-hi/ and use it, and with the build tools this course uses it does not even compile:
e: MainActivity.kt:46:45 Unresolved reference 'price_note'.
There would be nothing to fall back to on a phone that is not in Hindi, so the generated R class leaves it out.
Qualifiers, as a reference
| Folder | Applies when | Mandi Bhav uses it for |
|---|---|---|
values/ |
Always, as the fallback | Every string, colour and dimension |
values-hi/, values-mr/ |
Phone or app language is Hindi, Marathi | Translations |
values-night/ |
Dark theme is on | Colours that must change in the dark |
values-sw600dp/ |
Smallest screen width at least 600dp (tablets) | Bigger padding on tablets |
values-land/ |
Landscape | Rarely needed with Compose |
drawable/ |
Always | Vector images |
drawable-hdpi/ … drawable-xxxhdpi/ |
Screen density | Bitmap images, one per density |
mipmap-anydpi/ |
Always, API 26 and above | The adaptive launcher icon |
Qualifiers combine in a fixed order, such as values-hi-night/. Android chooses the best match for the current configuration when it loads resources. That is the reason for the previous lesson's behaviour: when the language, theme or orientation changes, the right resources may now be different, so Android recreates the activity and lets it load them again.
For a colour that changes in dark mode:
<!-- res/values/colors.xml -->
<resources>
<color name="ic_launcher_background">#2E7D32</color>
<color name="price_highlight">#2E7D32</color>
</resources>
<!-- res/values-night/colors.xml -->
<resources>
<color name="price_highlight">#81C784</color>
</resources>
colorResource(R.color.price_highlight) gives the dark green normally and the lighter green when dark theme is on. In a Compose app most colour comes from the MaterialTheme colour scheme instead, which module 4 sets up; resource colours are for the places that need them, such as the launcher icon background.
Drawables and screen density
Phones differ in how many pixels they pack into an inch. Android groups them into density buckets:
| Bucket | Scale | Pixels for a 48dp icon |
|---|---|---|
mdpi |
1× | 48 |
hdpi |
1.5× | 72 |
xhdpi |
2× | 96 |
xxhdpi |
3× | 144 |
xxxhdpi |
4× | 192 |
A bitmap (PNG or WebP) needs a copy per bucket, or Android scales the nearest one and it looks blurred or wastes memory. A vector drawable is a set of drawing instructions and is sharp at every size, in one file. The starter's ic_launcher_foreground.xml is a vector in plain drawable/, which is why it needs no density folders. Prefer vectors for icons and simple artwork; use bitmaps for photographs.
mipmap/ folders are for launcher icons only, because the launcher may show your icon larger than your app does. The starter has only mipmap-anydpi/ic_launcher.xml, an adaptive icon combining a background colour with the foreground vector. Adaptive icons arrived in Android 8.0 (API 26), and Mandi Bhav's minSdk is 26, so no bitmap fallbacks are needed. Lint warns that the icon "is missing a monochrome tag", which matters on newer Android versions that tint icons to match the wallpaper; adding that layer is part of the release work later.
Dimensions: dimens.xml or dp in Compose
In Compose you write sizes directly:
Modifier.padding(16.dp)
dp (density-independent pixels) means the same physical size on every density bucket. For text, use sp, which also scales with the user's font-size setting; many older users in particular raise it, and text in dp ignores their choice.
A dimens.xml resource is worth it only when the value must change with a qualifier:
<!-- res/values/dimens.xml -->
<resources>
<dimen name="screen_padding">16dp</dimen>
</resources>
<!-- res/values-sw600dp/dimens.xml -->
<resources>
<dimen name="screen_padding">32dp</dimen>
</resources>
dimensionResource(R.dimen.screen_padding) then gives 16dp on phones and 32dp on tablets. For a fixed value, a dimens.xml entry adds a file and an indirection for nothing; XML-era projects put every number there, and you do not need to copy them. Module 4 shows the Compose-native way to adapt to screen size, which often replaces this.
Check your work
Why can a hardcoded "Namaste from $name" not be translated? It is compiled into the code; only resources are looked up per language at run time.
What does %2$s mean? The second argument, as a string. Numbering lets a translation reorder arguments.
Why did "Onion" stay in English on the Hindi screen? It was passed in as data from Kotlin, not read from a resource.
Why does pluralStringResource take the count twice? Once to choose the plural form, once to fill in %d.
What happens on a Marathi phone if values-mr lacks a string? It falls back to values/ and shows English; lint reports MissingTranslation.
What happens if a string exists only in values-hi? It is missing from R, so using it fails to compile with Unresolved reference. The default folder must always be complete.
Practice 3: what do 1 and 0 show? "1 market found" and "0 markets found" in English, because English uses other for zero. In Hindi, 0 uses the one form.
Why does the starter's icon need no density folders? The foreground is a vector, and the adaptive icon needs API 26, which is the minSdk.
dp or sp for text? sp, so it follows the user's font-size setting.
When is dimens.xml worth it in Compose? When the value differs by qualifier, such as a tablet padding.
Practice
- Move every user-visible string in your
MainActivity.ktintostrings.xmland read them withstringResource. Search the Kotlin file for a"afterwards; only resource arguments and log tags should be left. - Add
values-hi/strings.xmlwith the names above, in Devanagari if you can type it or romanised if not, and switch the app to Hindi with theadbcommand. Confirm the screen changes without a rebuild, then switch it back. - Change
marketCountto 1 and then to 0, and look at both in English and in Hindi. Which plural form does each language choose for 0? - Add
values-night/colors.xml, turn on dark theme from quick settings, and watch the price colour change. With the previous lesson's logging in place, you would see the activity recreated, exactly as on rotation. - Break it on purpose: add
values-mr/strings.xmlcontaining onlygreeting, run./gradlew lintDebug, and read every error. Then either translate the rest or delete the folder. - Harder: add a string only to
values-hi/, use it inPriceSummary, and build. Read the error, add the English version tovalues/, and build again. Then say in one sentence which folder must always be complete, and why the build refused.
Official documentation
- Android Developers — App resources overview — The full qualifier table, in the order they must be combined, and how Android picks the best match.
- Android Developers — String resources — Strings, plurals and string arrays, including escaping apostrophes and formatting arguments.
- Android Developers — Localize your app — Default resources, testing in other locales, and why the default folder must be complete.
- Android Developers — Resources in Compose —
stringResource,pluralStringResource,dimensionResourceand the other Compose resource functions. - Android Developers — Support different pixel densities — Density buckets, dp, and when to use vectors.
Next: module 3, Jetpack Compose Fundamentals.
What you can do now
You can open an Android project and say what every file is for, and keep the generated ones out of git. You can read a Gradle build: plugins and libraries, the version catalog, the BOM, the three SDK numbers, and how build types and BuildConfig let one project produce a debug app talking to a test server and a release app talking to the real one. You can read and change the manifest, explain android:exported, and find what the libraries merged into it. You know why a Compose app has one activity, and you can still read and write the intent-based code older apps are made of. Most importantly, you have watched the lifecycle destroy a counter on rotation and lose a whole process in the background, so you know what "the app forgot" means and when it happens. And Mandi Bhav's text now lives in resources, ready for Hindi and Marathi.
The next module is Jetpack Compose Fundamentals: how setContent and the composables inside it actually describe a screen, what recomposition re-runs, and why the order of modifiers matters.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.
About the internship