How to Get Plural String Resources Outside of Jetpack Compose in Android
When developing modern Android apps with Jetpack Compose, the pluralStringResource() helper makes resolving quantity-based strings effortless inside your UI layer. However, you will often find yourself needing to resolve plurals outside of Composable functions—such as in a ViewModel, a background worker, or a plain Kotlin utility class.
If you are looking for the non-Composable equivalent to context.getString(R.string.id) for plurals, here is everything you need to know.
The Direct Solution: getQuantityString()
Outside of Composables, the Android platform provides the getQuantityString() method via the standard Resources object. You can access this directly through any valid Context instance.
val count = 5
val text = context.resources.getQuantityString(R.plurals.files, count, count)
Understanding the Parameters
Notice that count is passed twice in the snippet above. This is a common pattern in Android plural formatting:
- First parameter (
id): The resource ID of your plural string (e.g.,R.plurals.files). - Second parameter (
quantity): The integer used by Android's localization engine to determine which plural rule applies (such as zero, one, few, many, or other depending on the locale). - Subsequent parameters (
formatArgs): The values substituted into placeholders like%dor%sdefined inside your XML string.
Example: XML Resource and Kotlin Usage
Suppose you have the following definition in res/values/strings.xml:
<plurals name="unread_messages">
<item quantity="one">%d unread message</item>
<item quantity="other">%d unread messages</item>
</plurals>
Here is how you retrieve it outside of a Composable function:
class NotificationHelper(private val context: Context) {
fun getUnreadSummary(unreadCount: Int): String {
return context.resources.getQuantityString(
R.plurals.unread_messages,
unreadCount,
unreadCount
)
}
}
Architecture Tip: Avoid Context in ViewModels (Use UiText)
While passing a Context or Application reference into your classes is technically straightforward, holding a Context in a ViewModel can hinder unit testability and potentially cause memory leaks if not handled carefully.
Instead of formatting strings directly in your ViewModel, a common best practice in Jetpack Compose architecture is to wrap your string representations in a sealed interface (commonly called UiText):
sealed interface UiText {
data class DynamicString(val value: String) : UiText
data class PluralResource(
@PluralsRes val id: Int,
val quantity: Int,
val args: List<Any> = emptyList()
) : UiText
@Composable
fun asString(): String {
return when (this) {
is DynamicString -> value
is PluralResource -> pluralStringResource(id, quantity, *args.toTypedArray())
}
}
}
With this pattern, your ViewModel only emits a lightweight, testable data class:
val messageText = UiText.PluralResource(R.plurals.unread_messages, count, listOf(count))
Then, your Composable resolves it seamlessly without requiring an explicit Context in business logic:
Text(text = uiState.messageText.asString())
Summary
To resolve plural string resources outside of a Composable, call context.resources.getQuantityString(id, quantity, formatArgs...). If you are doing this within a ViewModel, consider delegating the resolution back to the UI layer using a state wrapper like UiText to keep your code clean, modular, and easy to unit test.