Customizing Composables

Coda Pizza is off to a great start. You are ready to start making your composables look just the way you want them to. Previously, you accomplished this using XML attributes. In Compose, function parameters take the place of the attributes that you are accustomed to in XML.

You have already seen a few parameters on the built-in composables you have been using: text for the Text composable and checked and onCheckedChange for Checkbox. You are also free to add parameters to your own composables.

Declaring inputs on a composable function

Think about the ToppingCell composable. It will need to take in three pieces of information: the name of the topping, the placement of the topping, and what to do when the topping is clicked. Currently, these values are hardcoded – the topping is always pineapple, it is placed on the whole pizza, and nothing happens when you try to edit the topping. This will upset opponents of pineapple on pizza, so it is time to make your toppings more flexible.

The set of toppings and the options for the position of toppings will both have a fixed set of values. Instead of representing these using Strings, enums are a better fit. Also, the hardcoded strings you have been using would not be easy to localize. Jetpack Compose supports loading from your string resources, and it is a good idea to use them.

So start by defining some string resources:

Listing 26.11  Adding string resources (strings.xml)

<resources>
    <string name="app_name">Coda Pizza</string>

    <string name="placement_none">None</string>
    <string name="placement_left">Left half</string>
    <string name="placement_right">Right half</string>
    <string name="placement_all">Whole pizza</string>

    <string name="topping_basil">Basil</string>
    <string name="topping_mushroom">Mushrooms</string>
    <string name="topping_olive">Olives</string>
    <string name="topping_peppers">Peppers</string>
    <string name="topping_pepperoni">Pepperoni</string>
    <string name="topping_pineapple">Pineapple</string>
</resources>

Next, create a new package called com.bignerdranch.android.codapizza.model to store the model classes you will use to define and represent a pizza. Create a new file in this package called ToppingPlacement.kt and define an enum to specify which part of a pizza a topping is present on.

Give the enum three cases: the whole pizza, the left half of the pizza, and the right half of the pizza. If a topping is not present on the pizza, you can represent that with a null value instead.

Listing 26.12  Specifying topping locations (ToppingPlacement.kt)

enum class ToppingPlacement(
    @StringRes val label: Int
) {
    Left(R.string.placement_left),
    Right(R.string.placement_right),
    All(R.string.placement_all)
}

(The @StringRes annotation is not required, but it helps Android Lint verify at compile time that constructor calls provide a valid string resource ID.)

Next, define another enum to specify all the toppings that a customer can add to their pizza. Place this enum in a new file called Topping.kt in the model package, and populate it as shown:

Listing 26.13  Specifying toppings (Topping.kt)

enum class Topping(
    @StringRes val toppingName: Int
) {
    Basil(
        toppingName = R.string.topping_basil
    ),

    Mushroom(
        toppingName = R.string.topping_mushroom
    ),

    Olive(
        toppingName = R.string.topping_olive
    ),

    Peppers(
        toppingName = R.string.topping_peppers
    ),

    Pepperoni(
        toppingName = R.string.topping_pepperoni
    ),

    Pineapple(
        toppingName = R.string.topping_pineapple
    )
}

With the models in place, you are ready to add parameters to ToppingCell. You will add three parameters: a topping, a nullable placement, and an onClickTopping callback. Be sure to provide values for these parameters in your preview composable, otherwise you will get a compiler error.

Listing 26.14  Adding parameters to a composable (ToppingCell.kt)

@Preview
@Composable
private fun ToppingCellPreview() {
    ToppingCell(
        topping = Topping.Pepperoni,
        placement = ToppingPlacement.Left,
        onClickTopping = {}
    )
}

@Composable
fun ToppingCell(
    topping: Topping,
    placement: ToppingPlacement?,
    onClickTopping: () -> Unit
) {
    ...
}

You will also need to update MainActivity to provide these arguments when it calls ToppingCell. Currently, MainActivity has a compiler error, which will prevent the preview from updating. Fix this now by specifying the required arguments for ToppingCell. You will revisit the onClickTopping callback later. For now, implement it with an empty lambda.

Listing 26.15  Fixing the compiler error (MainActivity.kt)

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ToppingCell(
                topping = Topping.Pepperoni,
                placement = ToppingPlacement.Left,
                onClickTopping = {}
            )
        }
    }
}

Return to ToppingCell.kt and build the project to update the preview. Thanks to the changes you just made to ToppingCellPreview, you might expect the preview to show pepperoni on just the left side of the pizza. However, it still shows pineapple on the whole pizza. This is because you have not yet used the new inputs in your ToppingCell. Let’s change that.

Resources in Compose

Start with the name of the topping. With the framework views you have seen before, you used the Context.getString(Int) function to turn a string resource into a String object you could show onscreen. In Compose, you can accomplish the same thing using the stringResource(Int) function. Take it for a spin.

Listing 26.16  Using string resources in Compose (ToppingCell.kt)

...
@Composable
fun ToppingCell(
    topping: Topping,
    placement: ToppingPlacement?,
    onClickTopping: () -> Unit
) {
    Row {
        Checkbox(
            checked = true,
            onCheckedChange = { /* TODO */ }
        )

        Column {
            Text(
                text = "Pineapple"
                text = stringResource(topping.toppingName)
            )

            Text(
                text = "Whole pizza"
            )
        }
    }
}

Build and refresh the preview. You should see that the topping name changes from the hardcoded Pineapple string to the Pepperoni string from your string resources. (If you wanted, you could also specify a specific string resource instead of accessing it in a variable. The same string lookup you just wrote could also be written as stringResource(R.string.pepperoni), but you instead read it from the topping parameter to keep your composable dynamic.)

Control flow in a composable

Next, shift your attention to the placement text. This is a bit trickier because the placement input is nullable. A null value indicates that the topping is not on the pizza. In that case, the second text should not be visible and the Checkbox should not be checked.

To add this null check, you can wrap the second Text in an if statement. If the topping is present, this if statement will execute and add the label to the UI. Otherwise, the if statement will be skipped, and only one Text will end up onscreen.

Go ahead and make this change now. While you are at it, update the checked input to Checkbox to check whether the topping is present on the pizza.

Listing 26.17  if statements in a composable (ToppingCell.kt)

...
@Composable
fun ToppingCell(
    topping: Topping,
    placement: ToppingPlacement?,
    onClickTopping: () -> Unit
) {
    Row {
        Checkbox(
            checked = true,
            checked = (placement != null),
            onCheckedChange = { /* TODO */ }
        )

        Column {
            Text(
                text = stringResource(topping.toppingName)
            )

            if (placement != null) {
                Text(
                    text = "Whole pizza"
                    text = stringResource(placement.label)
                )
            }
        }
    }
}

Refresh the preview once more and confirm that the placement text has updated to Left half, matching the value specified in ToppingCellPreview.

To confirm that your ToppingCell is appearing as expected when the topping is not present, you will need to update your preview function to specify a null input for the placement. You could adjust your existing preview to change the placement argument, but it can be helpful to preview several versions of a composable at the same time.

Create a second preview function to show what ToppingCell looks like when the topping is not added to the pizza. Give your two preview functions distinct names to clarify what they are previewing.

Listing 26.18  Adding another preview (ToppingCell.kt)

@Preview
@Composable
private fun ToppingCellPreviewNotOnPizza() {
    ToppingCell(
        topping = Topping.Pepperoni,
        placement = null,
        onClickTopping = {}
    )
}

@Preview
@Composable
private fun ToppingCellPreviewOnLeftHalf() {
    ToppingCell(
        topping = Topping.Pepperoni,
        placement = ToppingPlacement.Left,
        onClickTopping = {}
    )
}
...

Refresh the preview. You will now see two previews. In the one labeled ToppingCellPreviewNotOnPizza, only the Pepperoni label appears in the cell and the checkbox is unchecked (Figure 26.6).

Figure 26.6  No pepperoni, please

No pepperoni, please

You have just observed the effects of control flow inside a composable. Because composables are functions, you can call them however you would like – including conditionally. Here, the Text composable was not invoked, so it is not drawn onscreen.

You can accomplish something similar with framework views by setting their visibility to gone. But with a framework view, the View itself would still be there, just not contributing to what is drawn onscreen. In Compose, the composable is simply not invoked. It does not exist at all.

if statements are not the only control flows you can use in a composable function. Composable functions are, at their core, merely Kotlin functions, so any syntax you can use in other functions can appear in a composable. when expressions, for loops, and while loops are all fair game in your composables, to name a few examples.

Aligning elements in a row

Take another look at the preview of your topping cell. You may have noticed that in the unselected state, it looks a bit awkward because the checkbox and the text are not vertically aligned. Worry not, there is another parameter you can specify to beautify this layout.

The Row and Column composables specify their own parameters that you can use to adjust the layout of their children. For a Row, you can use the Alignment parameter to adjust how its children are positioned vertically. (A Column’s Alignment will adjust the horizontal positioning of its children.)

By default, Row’s vertical alignment is set to Alignment.Top, meaning that the top of each composable will be at the top of the row. To center all items in the composable, set its alignment to Alignment.CenterVertically.

Listing 26.19  Specifying alignment (ToppingCell.kt)

...
@Composable
fun ToppingCell(
    topping: Topping,
    placement: ToppingPlacement?,
    onClickTopping: () -> Unit
) {
    Row(
        verticalAlignment = Alignment.CenterVertically
    ) {
        ...
    }
}

Be sure to import androidx.compose.ui.Alignment from the options provided.

By the way: Do not confuse the Alignment parameter with the Arrangement parameter, which specifies how the extra horizontal space of a Row (or vertical space, for a Column) should be placed relative to its children.

Refresh the preview again and confirm that the topping name and checkbox are vertically aligned (Figure 26.7).

Figure 26.7  Aligning the contents of a row

Aligning the contents of a row

Specifying text styles

Composable parameters are useful for arranging your content and setting values to display. They also serve an important role in styling your UI.

In Chapter 9, you set the android:textAppearance attribute to ?attr/textAppearanceHeadline5 to apply built-in styling to text elements in XML. In Compose, you can accomplish the same thing by setting the style parameter of the Text composable. Like the framework toolkit, Compose also has built-in text styles accessible through the MaterialTheme object. Take them for a spin now, applying the body1 style to the name of the topping and body2 to the placement of the topping.

When entering this code, be sure to choose the MaterialTheme object when prompted, not the MaterialTheme composable function. Their imports are the same, so no need to worry if you choose the wrong one initially – just note that Android Studio will autocomplete different code. You will see how the MaterialTheme function works in Chapter 29.

Listing 26.20  Setting text styles (ToppingCell.kt)

...
@Composable
fun ToppingCell(
    ...
) {
    Row(
        verticalAlignment = Alignment.CenterVertically
    ) {
        ...
        Column {
            Text(
                text = stringResource(topping.toppingName),
                style = MaterialTheme.typography.body1
            )

            if (placement != null) {
                Text(
                    text = stringResource(placement.label),
                    style = MaterialTheme.typography.body2
                )
            }
        }
    }
}

Update the previews by pressing the Build & Refresh label in the banner that says The preview is out of date or by building the project. You will see that the first line of text is larger than the second (Figure 26.8). The difference is subtle, but we promise – they are different sizes.

Figure 26.8  Text with style

Text with style
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset