CompositionLocal

In this chapter, you have seen a few examples where nesting one composable inside another causes the inner composable’s appearance to change. Using the MaterialTheme composable, for example, causes all components nested within it to be aware of your theme and use its colors.

When you added the Text to your TopAppBar, you might have noticed something interesting. Despite the fact that you did not specify any parameters besides the text itself, the Text was appropriately styled with the correct font size and color to appear in your app bar. How did this happen?

When you build a UI using Jetpack Compose, a composition hierarchy is created at runtime for all your composables – much like the view hierarchy for framework UIs. Whenever you call a composable function to add a UI element to your app, a corresponding node is added to this composition hierarchy. You cannot get a reference to this hierarchy or its nodes, but they are still there to organize your composables. (Whereas View objects are themselves the nodes in a framework UI’s hierarchy, and you can get direct access to them whenever you like.)

For Coda Pizza, the top of the composition hierarchy looks something like Figure 29.13:

Figure 29.13  Coda Pizza’s UI hierarchy

Coda Pizza’s UI hierarchy

And ToppingsList and OrderButton have their own children, down to the Texts and Checkbox seen onscreen.

In Chapter 11, we warned you that nesting view hierarchies too deeply could degrade an app’s performance. But although Coda Pizza’s UI hierarchy is many layers deep, fear not. Jetpack Compose is substantially more efficient than Android’s framework UI toolkit when it comes to managing, laying out, and drawing UI elements. Deeply nested layouts are not a performance concern with Compose in the way they were for the other apps you built – which is also why you did not reach for a tool like ConstraintLayout in Coda Pizza.

The top of this hierarchy is your MaterialTheme composable. As you have seen in this chapter, using the MaterialTheme composable sets the values that will be returned by the MaterialTheme object. And these values have a scope.

When you call the MaterialTheme composable, the theme values are stored and made accessible for all its children. Whenever a component needs to access a theme attribute, you use the MaterialTheme object and access the colors, shapes, or typography with code like MaterialTheme.colors.primary. Values referenced in this way are tracked by instances of a class called CompositionLocal so that every composable that is a child of the MaterialTheme node can access your theme information.

You placed the MaterialTheme composable at the root of your composition because you want the theme to affect everything you display in your composition. If you added a composable as a sibling to the MaterialTheme, it would be unaware of the themes you set elsewhere in the UI hierarchy and would use the default material theme.

If for some reason you want to have different themes for different parts of your composition, you can also nest one MaterialTheme composable inside another. The inner theme will override the theme values from the outer MaterialTheme composable, but only for the children of the inner MaterialTheme.

Back to the question of where the style of the Text in your TopAppBar is coming from. In addition to the theme attributes set in the themes.xml resource file and AppTheme composable (if you have one), some composables also specify preferred theme attributes that Compose will take into account. (These are officially termed current theme attributes, but we find that term unnecessarily confusing.)

For example, while your application has text color specifications, the TextButton composable you are using for the topping placement options in your dialog overrides this color specification, setting a style that works with the overall theme but is specific to its own environment. When the Text child of your TextButtons asks for a text color, it gets this overridden value, not the value from your theme.

TopAppBar does the same thing to its text, setting a color that works with the background color set by the app theme. In this way, “current” theme attributes like the ones set by TextButton and TopAppBar provide automatic localized theming that coordinates with the app’s overall theme. The Text composable determines its styles by first looking at the text size, color, and so on set by its parent (or another direct ancestor) and then falling back to the theme for any styles that are not set.

Behind the scenes, these behaviors are all driven by the same CompositionLocal class we mentioned earlier.

CompositionLocals are variables that are defined for a part of your composition hierarchy. When a CompositionLocal is defined, it is accessible to all of its composable children – and can be overridden deeper in the hierarchy if the same CompositionLocal is set again. Theme information is often defined this way – many children need to share and access theme information, and CompositionLocals make that sharing easy.

Instances of CompositionLocal propagate theme information automatically. But you can also access CompositionLocal variables yourself to get many more values and resources associated with your composition. To see this in action, it is time to implement one last feature in Coda Pizza: the PLACE ORDER button.

Unfortunately, Coda Pizza will not result in a pizza being delivered to your address. But it can present you with a Toast (which, arguably, has many similarities to pizza). To set one up, you will replace your final TODO, which is lingering in OrderButton’s onClick callback.

First, prepare Coda Pizza to display a toast by adding a string resource for the message that will appear when the order is placed.

Listing 29.17  A consolation toast’s message (strings.xml)

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

  <string name="place_order_button">Place Order (%1$s)</string>
  <string name="order_placed_toast">Order submitted!</string>
  ...
</resources>

To show the toast, you need to obtain a Context. You could accomplish this by adding a context parameter to OrderButton and passing your activity context all the way down your composition hierarchy, but that would be messy and would not scale well if you needed to access many properties.

Instead, Compose includes a CompositionLocal out of the box that stores the context that hosts your composable UI. You can use this CompositionLocal to access the context regardless of where you are in the composition.

To read the value of a CompositionLocal variable, you first obtain a reference to the corresponding CompositionLocal class itself. Then you can get the value of the variable for the current position in the composition hierarchy via its current property. The convention for naming a CompositionLocal is to use the prefix Local followed by the name or type of the variable being provided. So the composition’s local context is stored in LocalContext.

Using the LocalContext property, obtain a Context. Then, implement your OrderButton’s onClick lambda to show a toast. Because CompositionLocals give you the current value of the variable, they can only be read inside the composition itself. This means that you must obtain the context outside the click listener, because your click listener cannot access the composition hierarchy.

Listing 29.18  Using a Context inside a composable (PizzaBuilderScreen.kt)

...
@Composable
private fun OrderButton(
    pizza: Pizza,
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    Button(
        modifier = modifier,
        onClick = {
            // TODO
            Toast.makeText(context, R.string.order_placed_toast, Toast.LENGTH_LONG)
                .show()
        }
    ) {
        val currencyFormatter = remember { NumberFormat.getCurrencyInstance() }
        val price = currencyFormatter.format(pizza.price)
        Text(text = stringResource(R.string.place_order_button, price))
    }
}

Run Coda Pizza and press the PLACE ORDER button. You should see a toast with the message Order submitted! near the bottom of the screen (Figure 29.14).

Figure 29.14  A toast to pizza

A toast to pizza

CompositionLocals are a convenient way to get more information about your composition. Many CompositionLocals are predefined and readily available should you need them. Some of these values, like your theme, allow you to easily customize portions of your UI hierarchy. Other CompositionLocals, meanwhile, have values that do not change in the composition hierarchy and can give you information about the composition itself.

Using the built-in CompositionLocals, you can access values including the Lifecycle of the component hosting your composition, the clipboard, and the size of the display. And because CompositionLocals are tracked by Compose itself, you can access all these values without declaring new parameters. This flexibility makes CompositionLocals a great choice for storing information you need to access sporadically throughout your UI.

You can also define your own CompositionLocals, if you need to. This is not something you will do for Coda Pizza, but if you want to know more about this process, take a look at the section called For the More Curious: Creating Your Own CompositionLocals near the end of this chapter.

..................Content has been hidden....................

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