Home > Blockchain >  How to use sealed class for placeholder values in string resource
How to use sealed class for placeholder values in string resource

Time:06-17

Is it possible within Jetpack Compose to use a sealed class to display strings with different values in their placeholders? I got confused when trying to figure out what to use for the Text objects. i.e. text = stringResource(id = it.?)

strings.xml

<string name="size_placeholder">Size %1$d</string>
<string name="sizes_placeholder_and_placeholder">Sizes %1$d and %2$d</string>

MainActivity.kt

    sealed class Clothes {
      data class FixedSizeClothing(val size: String, val placeholder: String): Clothes()
    
      data class MultiSizeClothing(val sizes: String, val placeholders: List<String>): Clothes()
    }


    @Composable
    fun ClothesScreen() {
        val clothingItems = remember { listOf(
            Clothes.FixedSizeClothing(itemSize = stringResource(id = R.string.size), itemPlaceholder = "8"),
            Clothes.MultiSizeClothing(itemSizes = stringResource(id = R.string.sizes), itemPlaceholders = listOf("0", "2"))
        )
    }

    Scaffold(
        topBar = { ... },
        content = { it ->
            Row {
                LazyColumn(
                    modifier = Modifier.padding(it)
                ) {
                    items(items) {
                        Column() {
                            Text(
                                text = stringResource(id = it.?)
                            )
                            Text(
                                text = stringResource(id = it.?)
                            )
                        }
                    }
                }
            }
        },
        containerColor = MaterialTheme.colorScheme.background
    )

expected result

enter image description here

CodePudding user response:

This is more a question about how to check for the type of a subclass of a sealed class (or sealed interface). Just to avoid any confusion, it should be made clear that these are Kotlin features and are not related to Jetpack Compose.

But yes, they can be used inside Composables as well or anywhere you want, really.

You would use a when (...) expression on the value of your sealed class to determine what to do based on the (sub)type of your sealed class (it works the same for sealed interfaces). Inside the when expression you then use the is operator to check for different subtypes.

val result = when (it) {
    is Clothes.FixedSizeClothing -> {
        // it.size and it.placeholder are accessible here due to Kotlin smart cast
        // do something with them...
        // last line will be returned as the result
    }
    is Clothes.MultiSizeClothing -> {
        // it.sizes and it.placeholders are accessible here due to Kotlin smart cast
        // do something with them...
        // last line will be returned as the result
    } 

In situations when you don't need the result you just omit the val result = part. Note that the name result is arbitrary, pick whatever best describes the value you are creating.

The advantage of this type of the when expression is that it will give you a warning (and in future versions an error) if you forget one of the subtypes inside the when expression. This means the when expression is always exhaustive when there is no warning present, i.e. the Kotlin compiler checks at compile-time for all subtypes of the specific sealed class that are defined inside your whole codebase and makes sure that you are accounting for all of them inside the when expression.

For more on sealed classes inside a when expression see https://kotlinlang.org/docs/sealed-classes.html#sealed-classes-and-when-expression

In your case, you would do the same to generate the text value that you would then pass into the Text(text = ...) composable.

    Scaffold(
        topBar = { ... },
        content = { it ->
            Row {
                LazyColumn(
                    modifier = Modifier.padding(it)
                ) {
                    items(clothingItems) {
                        val text = when (it) {
                            is Clothes.FixedSizeClothing ->
                                stringResource(id = R.string.size, it.placeholder)
                            is Clothes.MultiSizeClothing ->
                                stringResource(id = R.string.sizes, it.placeholders[0], it.placeholders[1])
                        }
                        Text(text = text)
                    }
                }
            }
        },
        containerColor = MaterialTheme.colorScheme.background
    )

I used the stringResource(@StringRes id: Int, vararg formatArgs: Any): String version of the call above to construct the final text. See here for other options available in Compose https://developer.android.com/jetpack/compose/resources

If you do want to store the presentation String inside your data classes as you are trying to do in your example you could store the resource id instead of the resolved String. Also since you are using integer placeholders (%1$d) in your resource strings, the type of your size and sizes values can be Int. So all together that would be something like this

import androidx.annotation.StringRes

sealed class Clothes {
    data class FixedSizeClothing(@StringRes val size: Int, val placeholder: Int): Clothes()

    data class MultiSizeClothing(@StringRes val sizes: Int, val placeholders: List<Int>): Clothes()
}

And then when you define the items you would not call stringResource(id = ...) anymore and your size and sizes values are just integers.

val clothingItems = remember {
    listOf(
        Clothes.FixedSizeClothing(size = R.string.size, placeholder = 8),
        Clothes.MultiSizeClothing(sizes = R.string.sizes, placeholders = listOf(0, 2))
    )
}

The added benefit of this is that now you do not need a Composable context (or a Context or a Resources reference) to create instances of your Clothes sealed class.

Then when declaring the UI, you would do something like this

LazyColumn(
    modifier = Modifier.padding(it)
) {
    items(clothingItems) {
        val text = when (it) {
            is Clothes.FixedSizeClothing ->
                stringResource(id = it.size, it.placeholder)
            is Clothes.MultiSizeClothing ->
                stringResource(id = it.sizes, it.placeholders[0], it.placeholders[1])
        }
        Text(text = text)
    }
}
  • Related