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 %d or %s defined 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.