Bart Kessels
Bart Kessels
Passionate open source software engineer who loves to go backpacking

Plural string resource in your Android app

Plural string resource in your Android app
This image is generated using Dall-E
  • Prompt: Generate an image of a phone displaying an xml file with multiple lines of text with numbers in a minimalistic flat style
  • In a previous post about supporting multiple languages, we’ve discussed the use of string resources. Which are XML-files where you store the strings that are used in your app.

    This is a great way to support multiple languages, but what if you want to display a string to the user that is different for multiple amounts? For example, you want to display the string You have clicked 1 time or You have clicked 2 times.

    For this blogpost we’ll be contuining with the app we’ve created in the Create an Android app story, only on a different branch. We’ll be using the feature/plural-string-resource branch.

    Update the ViewModel

    Before we’ll add the string resource, we need to update our view model to keep track of the number of times we’ve clicked the button. We’ll be adding a new property to our view model called clickCount.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    // ... redacted
    
    class MainViewModel: ViewModel() {
        private var clickCount = 0
        private val _displayText = MutableStateFlow<MainState>(MainState.Uninitialized)
        val displayText = _displayText.asStateFlow()
    
        fun updateText() {
            clickCount++
            _displayText.update { MainState.Updated }
        }
    }
    
    // ... redacted
    

    Now, if we run our application we don’t see any change. This is because we need to update our view state to include the clickCount property. We’ll be updating our Updated implementation of MainState sealed class to include a new state called UpdatedWithCount. We only need this property on the Updated state because when the user hasn’t pressed the button we don’t want to change the current state of the app.

    1
    2
    3
    4
    5
    6
    
    // ... redacted
    
    sealed interface MainState {
        data object Uninitialized: MainState
        data class Updated(val clickCount: Int): MainState
    }
    

    This will also lead to another update to our updateText method in the view model, because we need to give the clickCount property as a constructor parameter to the MainState.Updated class we’ve just updated.

    1
    2
    3
    4
    5
    6
    7
    8
    
    // ... redacted 
    
        fun updateText() {
            clickCount++
            _displayText.update { MainState.Updated(clickCount) }
        }
        
    // ... redacted
    

    If we try to compile our app, we see that the MainInternal composable function fails to compile. This is because of the when statement.

    Update the composable

    To make our when statement compile again, we need to tell the compiler that the ViewState.Updated is a type instead of a singleton object. We can do this by adding a is statement to the when statement.

    1
    2
    3
    4
    5
    6
    7
    8
    
    // ... redacted
    
        val textToDisplay = when (state) {
            MainState.Uninitialized -> R.string.uninitialized_text
            is MainState.Updated -> R.string.updated_text
        }
        
    // ... redacted
    

    Update the strings resource

    Now that we’ve updated our logic and our composable, we can start implementing the plural strings.

    If we take a look at the documentation for plural strings, we see that we have multiple options for the quantity that we want to support, such as zero, one, two, few, many and other. For our use case, we only need to support one and other (Google, 2024).

    Let’s update our updated_text resource to make use of the plural string.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    <resources>
        <!-- ... redacted -->
        
        <plurals name="updated_text">
            <item quantity="one">You have clicked 1 time</item>
            <item quantity="other">You have clicked %d times</item>
        </plurals>
        
        <!-- ... redacted -->
    </resources>
    

    As you can see, we’ve updated the <string name=""> element to a <plurals name=""> element. This gives us extra elements to work with multiple quantities. In our example we’ve only defined the quantity for one and other. The other quantity will be taken whenever the quantity doesn’t match one.

    To use our plural string, we need to update our Main composable to use the quantityStringResource instead of the stringResource method.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    // ... redacted
    
        val textToDisplay = when (state) {
            MainState.Uninitialized -> stringResource(R.string.uninitialized_text)
            is MainState.Updated -> pluralStringResource(R.plurals.updated_text, state.clickCount)
        }
    
        Column {
            Text(textToDisplay)
            Button(onClick = onButtonClick) {
                Text(stringResource(R.string.update_text_button))
            }
        }
    
    // ... redacted
    

    We’ve changed the return type of our when statement from the resource id to the actual string, because our plural string resource needs to be accessed using the pluralStringResource method instead of the stringResource we need for the regular string resource. The pluralStringResource also required an extra parameter, the quantity. Since we’ll be using the clickCount property from our Updated state, we can use that as the quantity.

    If we run our application, and click the button a couple of times we see that our text is changing from Hello world! to You have clicked 1 time and You have clicked 2 times.

    Update the test cases

    Because our application already contains UI tests, we need to make sure that they keep passing as well. So, last but definitely not least, we should also update our test cases.

    Let’s update the shouldDisplayHelloAgainWhenTheMainStateIsUpdated test case in our MainTest to shouldDisplayYouHaveClickedOnceWhenTheMainStateIsUpdatedWithAClickCountOfOne and add another test case to validate the text when the click count is five.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
    // ... redacted
    
        @Test
        fun shouldDisplayYouHaveClickedOnceWhenTheMainStateIsUpdatedWithAClickCountOfOne() {
            // Arrange & Act
            composeTestRule.setContent {
                MainInternal(
                    state = MainState.Updated(clickCount = 1),
                    onButtonClick = { }
                )
            }
    
            // Assert
            composeTestRule
                .onNodeWithText("You have clicked 1 time")
                .assertIsDisplayed()
        }
    
        @Test
        fun shouldDisplayYouHaveClickedXTimesWhenTheMainStateIsUpdatedWithAClickCountOfFive() {
            // Arrange
            val noClicks = 5
    
            // Act
            composeTestRule.setContent {
                MainInternal(
                    state = MainState.Updated(clickCount = noClicks),
                    onButtonClick = { }
                )
            }
    
            // Assert
            composeTestRule
                .onNodeWithText("You have clicked 5 times")
                .assertIsDisplayed()
        }
    
    // ... redacted
    

    You might notice that we moved from the context.stringResource to hardcoded strings for these test cases. This is something I did because it allows us to validate that our plural string resource is correct, and that the correct plural is used for the amount we test. If for example, we’ve swapped the text for the one and other quantity our test would fail in this case. If we use the context.pluralStringResource, we wouldn’t detect that issue.

    Categories