Jetpack Compose Row and Column Layouts

User interface design is largely a matter of selecting the appropriate interface components, deciding how those views will be positioned on the screen, and then implementing navigation between the different screens of the app.

As is to be expected, Compose includes a wide range of user interface components for use when developing an app. Compose also provides a set of layout composables to define both how the user interface is organized and how the layout responds to factors such as changes in screen orientation and size.

This chapter will introduce the Row and Column composables included with Compose and explain how these can be used to create user interface designs with relative ease.

Creating the RowColDemo project

Launch Android Studio and select the New Project option from the welcome screen. Within the resulting new project dialog, choose the Empty Compose Activity template before clicking on the Next button.

Enter RowColDemo into the Name field and specify com.example.rowcoldemo as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo).

Within the MainActivity.kt file, delete the Greeting function and add a new empty composable named MainScreen:

@Composable
fun MainScreen() {
    
}

Next, edit the onCreateActivity() method and DefaultPreview function to call MainScreen instead of Greeting. As we work through the examples in this chapter, row and column-based layouts will be built using instances of a custom component named TextCell which displays text within a black border with a small amount of padding to provide space between adjoining components. Before proceeding, add this function to the MainActivity.kt file as follows:

.
.
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
.
.
@Composable
fun TextCell(text: String, modifier: Modifier = Modifier) {
 
    val cellModifier = Modifier
        .padding(4.dp)
        .size(100.dp, 100.dp)
        .border(width = 4.dp, color = Color.Black)
 
    Text(text = text, cellModifier.then(modifier), 
                fontSize = 70.sp,
                fontWeight = FontWeight.Bold,
                textAlign = TextAlign.Center)
}

Row composable

The Row composable, as the name suggests, lays out its children horizontally on the screen. For example, add a simple Row composable to the MainScreen function as follows:

.
.
@Composable
fun MainScreen() {
    Row {
        TextCell("1")
        TextCell("2")
        TextCell("3")
    }
}

When rendered, the Row declared above will appear as illustrated in Figure 25-1 below:

Figure 25-1

Column composable

The Column composable performs the same purpose as the Row with the exception that its children are arranged vertically. The following example places the same three composables within a Column:

.
.
@Composable
fun MainScreen() {
    Column {
        TextCell("1")
        TextCell("2")
        TextCell("3")
    }
}

The rendered output from the code will appear as shown in Figure 25-2:

Figure 25-2

Combining Row and Column composables

Row and Column composables can, of course, be embedded within each other to create table style layouts. Try, for example, the following composition containing a mixture of embedded Row and Column layouts:

@Composable
fun MainScreen() {
    Column {
        Row {
            Column {
                TextCell("1")
                TextCell("2")
                TextCell("3")
            }
 
            Column {
                TextCell("4")
                TextCell("5")
                TextCell("6")
            }
 
            Column {
                TextCell("7")
                TextCell("8")
            }
        }
 
        Row {
            TextCell("9")
            TextCell("10")
            TextCell("11")
        }
    }
}

Figure 25-3 illustrates the layout generated by the above code:

Figure 25-3

Using this technique, Row and Column layouts may be embedded within each other to achieve just about any level of layout complexity.

Layout alignment

Both the Row and Column composables will occupy an area of space within the user interface layout depending on child elements, other composables, and any size-related modifiers that may have been applied. By default, the group of child elements within a Row or Column will be aligned with the top left-hand corner of the content area (assuming the app is running on a device configured with a left-to-right reading locale). We can see this effect if we increase the size of our original example Row composable:

@Composable
fun MainScreen() {
    Row(modifier = Modifier.size(width = 400.dp, height = 200.dp)) {
        TextCell("1")
        TextCell("2")
        TextCell("3")
    }
}

Before making this change, the Row was wrapping its children (in other words sizing itself to match the content). Now that the Row is larger than the content we can see that the default alignment has placed the children in the top left-hand corner of the Row component:

Figure 25-4

This default alignment in the vertical axis may be changed by passing through a new value using the verticalAlignment parameter of the Row composable. For example, to position the children in the vertical center of the available space, the Alignment.CenterVertically value would be passed to the Row as follows:

.
.
import androidx.compose.ui.Alignment
.
.
@Composable
fun MainScreen() {
    Row(verticalAlignment = Alignment.CenterVertically, 
        modifier = Modifier.size(width = 400.dp, height = 200.dp)) {
        TextCell("1")
        TextCell("2")
        TextCell("3")
    }
}

This will cause the content to be positioned in the vertical center of the Row’s area as illustrated below:

Figure 25-5

The following is a list of alignment values accepted by the Row vertical alignment parameter:

  • Alignment.Top – Aligns the content at the top of the Row content area.
  • Alignment.CenterVertically – Positions the content in the vertical center of the Row content area.
  • Alignment.Bottom – Aligns the content at the bottom of the Row content area.

When working with the Column composable, the horizontalAlignment parameter is used to configure alignment along the horizontal axis. Acceptable values are as follows:

  • Alignment.Start – Aligns the content at the horizontal start of the Column content area.
  • Alignment.CenterHorizontally – Positions the content in the horizontal center of the Column content area
  • Alignment.End – Aligns the content at the horizontal end of the Column content area.

In the following example, the Column’s children have been aligned with the end of the Column content area:

@Composable
fun MainScreen() {
    Column(horizontalAlignment = Alignment.End, 
           modifier = Modifier.width(250.dp)) {
        TextCell("1")
        TextCell("2")
        TextCell("3")
    }
}

When rendered, the resulting column will appear as shown in Figure 25-6:

Figure 25-6

When working with alignment it is worth remembering that it works on the opposite axis to the flow of the containing composable. For example, while the Row organizes children horizontally, alignment operates on the vertical axis. Conversely, alignment operates on the horizontal axis for the Column composable while children are arranged vertically. The reason for emphasizing this point will become evident when we introduce arrangements.

Layout arrangement positioning

Unlike the alignment settings, arrangement controls child positioning along the same axis as the container (i.e. horizontally for Rows and vertically for Columns). Arrangement values are set on Row and Column instances using the horizontalArrangement and verticalArrangement parameters respectively. Arrangement properties can be categorized as influencing either position or child spacing.

The following positional settings are available for the Row component via the horizontalArrangement parameter:

  • Arrangement.Start – Aligns the content at the horizontal start of the Row content area.
  • Arrangement.Center – Positions the content in the horizontal center of the Row content area.
  • Arrangement.End – Aligns the content at the horizontal end of the Row content area. The above settings can be visualized as shown in Figure 25-7:

Figure 25-7

The Column composable, on the other hand, accepts the following values for the verticalArrangement parameter:

  • Arrangement.Top – Aligns the content at the top of the Column content area.
  • Arrangement.Center – Positions the content in the vertical center of the Column content area.
  • Arrangement.Bottom – Aligns the content at the bottom of the Column content area. Figure 25-8 illustrates these verticalArrangement settings:

Figure 25-8

Using our example once again, the following change moves the child elements to the end of the Row content area:

Row(horizontalArrangement = Arrangement.End, 
        modifier = Modifier.size(width = 400.dp, height = 200.dp)) {
        TextCell("1")
        TextCell("2")
        TextCell("3")
}

The above code will generate the following user interface layout:

Figure 25-9

Similarly, the following positions child elements at the bottom of the containing Column:

Column(verticalArrangement = Arrangement.Bottom, 
        modifier = Modifier.height(400.dp)) {
    TextCell("1")
    TextCell("2")
    TextCell("3")
}

The above composable will render within the Preview panel as illustrated in Figure 25-10 below:

Figure 25-10

Layout arrangement spacing

Arrangement spacing controls how the child components in a Row or Column are spaced across the content area. These settings are still defined using the horizontalArrangement and verticalArrangement parameters, but require one of the following values:

  • Arrangement.SpaceEvenly – Children are spaced equally, including space before the first and after the last child.
  • Arrangement.SpaceBetween – Children are spaced equally, with no space allocation before the first and after the last child.
  • Arrangement.SpaceAround – Children are spaced equally, including half spacing before the first and after the last child.

In the following declaration, the children of a Row are positioned using the SpaceEvenly setting:

Row(horizontalArrangement = Arrangement.SpaceEvenly, 
                     modifier = Modifier.width(1000.dp)) {
        TextCell("1")
        TextCell("2")
        TextCell("3")
}

The above code gives us the following layout with equal gaps at the beginning and end of the row and between each child:

Figure 25-11

Figure 25-12, on the other hand, shows the same row configured with the SpaceBetween setting. Note that the row has no leading or trailing spacing:

Figure 25-12

Finally, Figure 25-13 shows the effect of applying the SpaceAround setting which adds full spacing between children and half the spacing on the leading and trailing ends:

Figure 25-13

Row and Column scope modifiers

The children of a Row or Column are said to be within the scope of the parent. These two scopes (RowScope and ColumnScope) provide a set of additional modifier functions that can be applied to change the behavior and appearance of individual children within a Row or Column. The Android Studio code editor provides a visual indicator when children are within a scope. In Figure 25-14, for example, the editor indicates that the RowScope modifier functions are available to the three child composables:

Figure 25-14

When working with the Column composable, a similar ColumnScope indicator will appear.

ColumnScope includes the following modifiers for controlling the position of child components:

  • Modifier.align() – Allows the child to be aligned horizontally using Alignment.CenterHorizontally, Alignment. Start, and Alignment.End values.
  • Modifier.alignBy() – Aligns a child horizontally with other siblings on which the alignBy() modifier has also been applied.
  • Modifier.weight() – Sets the height of the child relative to the weight values assigned to its siblings.

RowScope provides the following additional modifier functions to Row children:

  • Modifier.align() – Allows the child to be aligned vertically using Alignment.CenterVertically, Alignment.Top, and Alignment.Bottom values.
  • Modifier.alignBy() – Aligns a child with other siblings on which the alignBy() modifier has also been applied. Alignment may be performed by baseline or using custom alignment line configurations.
  • Modifier.alignByBaseline() – Aligns the baseline of a child with any siblings that have also been configured by either the alignBy() or alignByBaseline() modifier.
  • Modifier.paddingFrom() – Allows padding to be added to the alignment line of a child.
  • Modifier.weight() – Sets the width of the child relative to the weight values assigned to its siblings.

The following Row declaration, for example, sets different alignments on each of the three TextCell children:

Row(modifier = Modifier.height(300.dp)) {
    TextCell("1", Modifier.align(Alignment.Top))
    TextCell("2", Modifier.align(Alignment.CenterVertically))
    TextCell("3", Modifier.align(Alignment.Bottom))
}

When previewed, this will generate a layout resembling Figure 25-15:

Figure 25-15

The baseline alignment options are especially useful for aligning text content with differing font sizes. Consider, for example, the following Row configuration:

Row {
  Text(
      text = "Large Text",
      fontSize = 40.sp,
      fontWeight = FontWeight.Bold
  )
  Text(
      text = "Small Text",
      fontSize = 32.sp,
      fontWeight = FontWeight.Bold
  )
}

This code consists of a Row containing two Text composables, each using a different font size resulting in the following layout:

Figure 25-16

The Row has aligned the two Text composables along their top edges causing the text content to be out of alignment relative to the text baselines. To resolve this problem we can apply the alignByBaseline() modifier to both children as follows:

Row {
    Text(
      text = "Large Text",
      Modifier.alignByBaseline(),
      fontSize = 40.sp,
      fontWeight = FontWeight.Bold
    )
    Text(
      text = "Small Text",
      Modifier.alignByBaseline(),
      fontSize = 32.sp,
      fontWeight = FontWeight.Bold,
    )
}

Now when the layout is rendered, the baselines of the two Text composables will be aligned as illustrated in Figure 25-17:

Figure 25-17

As an alternative, the alignByBaseline() modifier may be replaced by a call to the alignBy() function, passing through FirstBaseline as the alignment parameter:

Modifier.alignBy(FirstBaseline)

When working with multi-line text, passing LastBaseline through to the alignBy() modifier function will cause appropriately configured sibling components to align with the baseline of the last line of text:

.
.
import androidx.compose.ui.layout.LastBaseline
.
.
@Composable
fun MainScreen() {
    Row {
        Text(
            text = "Large Text\nMore Text",
            Modifier.alignBy(LastBaseline),
            fontSize = 40.sp,
            fontWeight = FontWeight.Bold
        )
        Text(
            text = "Small Text",
            Modifier.alignByBaseline(),
            fontSize = 32.sp,
            fontWeight = FontWeight.Bold,
        )
    }
}

Now when the layout appears the baseline of the text content of the second child will align with the baseline of the last line of text in the first child:

Figure 25-18

Using the FirstBaseline in the above example would, of course, align the baseline of the small text composable with the baseline of the first line of text in the multi-line component:

Figure 25-19

In the examples we have looked at so far we have specified the baseline as the alignment line for both children. If we need the alignment to be offset for a child, we can do so using the paddingFrom() modifier. The following example adds an additional 80dp vertical offset to the first baseline alignment position of the small text composable:

@Composable
fun MainScreen() {
    Row {
        Text(
            text = "Large Text\nMore Text",
            Modifier.alignBy(FirstBaseline),
            fontSize = 40.sp,
            fontWeight = FontWeight.Bold
        )
        Text(
            text = "Small Text",
            modifier = Modifier.paddingFrom(
                alignmentLine = FirstBaseline, before = 80.dp, after = 0.dp),
            fontSize = 32.sp,
            fontWeight = FontWeight.Bold
        )
    }
}

When rendered, the above layout will appear as shown in Figure 25-20:

Figure 25-20

Scope modifier weights

The RowScope weight modifier allows the width of each child to be specified relative to its siblings. This works by assigning each child a weight percentage (between 0.0 and 1.0). Two children assigned a weight of 0.5, for example, would each occupy half of the available space. Modify the MainScreen function one last time as follows to demonstrate the use of the weight modifier:

@Composable
fun MainScreen() {
    Row {
        TextCell("1", Modifier.weight(weight = 0.2f, fill = true))
        TextCell("2", Modifier.weight(weight = 0.4f, fill = true))
        TextCell("3", Modifier.weight(weight = 0.3f, fill = true))
    }
}

Rebuild and refresh the preview panel, at which point the layout should resemble that shown in Figure 25-21 below:

Figure 25-21

Siblings that do not have a weight modifier applied will appear at their preferred size leaving the weighted children to share the remaining space.

ColumnScope also provides align(), alignBy(), and weight() modifiers, though these all operate on the horizontal axis. Unlike RowScope, there is no concept of baselines when working with ColumnScope.

Summary

The Compose Row and Column components provide an easy way to layout child composables in horizontal and vertical arrangements. When embedded within each other, the Row and Column allow table style layouts of any level of complexity to be created. Both layout components include options for customizing the alignment, spacing, and positioning of children. Scope modifiers allow the positioning, and sizing behavior of individual children to be defined, including aligning and sizing children relative to each other.

How to Use Modifiers in Jetpack Compose

In this chapter, we will introduce Compose modifiers and explain how they can be used to customize the appearance and behavior of composables. Topics covered will include an overview of modifiers and an introduction to the Modifier object. The chapter will also explain how to create and use modifiers, and how to add modifier support to your own composables.

An overview of modifiers

Many composables accept one or more parameters that define their appearance and behavior within the running app. We can, for example, specify the font size and weight of a Text composable by passing through parameters as follows:

@Composable
fun DemoScreen() {
    Text(
         "My Vacation", 
         fontSize = 40.sp,
         fontWeight = FontWeight.Bold
    )
}

In addition to parameters of this type, most built-in composables also accept an optional modifier parameter which allows additional aspects of the composable to be configured. Unlike parameters, which are generally specific to the type of composable (a font setting would have no meaning to a Column component for example), modifiers are more general in that they can be applied to any composable.

The foundation for building modifiers is the Modifier object. Modifier is a built-in Compose object designed to store configuration settings that can be applied to composables. The Modifier object provides a wide selection of methods that can be called upon to configure properties such as borders, padding, background, size requirements, event handlers, and gestures to name just a few. Once declared, a Modifier can be passed to other composables and used to change appearance and behavior.

In the remainder of this chapter, we will explore the key concepts of modifiers and demonstrate their use within an example project.

Creating the ModifierDemo project

Launch Android Studio and select the New Project option from the welcome screen. Within the new project dialog, choose the Empty Compose Activity template before clicking on the Next button.

Enter ModifierDemo into the Name field and specify com.example.modifierdemo as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo). Once the project has been created, the project files should be listed in the Project tool window located along the left-hand edge of the Android Studio main window.

Load the MainActivity.kt file into the code editor and delete the Greeting composable before making the following changes:

package com.example.modifierdemo
.
.
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
import androidx.compose.ui.text.font.FontWeight
.
.
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ModifierDemoTheme {
                Surface(color = MaterialTheme.colors.background) {
                    DemoScreen()
                }
            }
        }
    }
}
 
@Composable
fun DemoScreen() {
    Text(
        "Hello Compose", 
        fontSize = 40.sp,
        fontWeight = FontWeight.Bold
    ) 
}
 
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    ModifierDemoTheme {
        DemoScreen()
    }
}

Creating a modifier

The first step in learning to work with modifiers is to create one. To begin with, we can create a modifier without any configuration settings as follows:

val modifier = Modifier

This essentially gives us a blank modifier containing no configuration settings. To configure the modifier, we need to call methods on it. For example, the modifier can be configured to add 10dp of padding on all four sides of any composable to which it is applied:

val modifier = Modifier.padding(all = 10.dp)

Method calls on a Modifier instance may be chained together to apply multiple configuration settings in a single operation. The following addition to the modifier will draw a black, 2dp wide border around a composable:

val modifier = Modifier
    .padding(all = 10.dp)
    .border(width = 2.dp, color = Color.Black)

Once a modifier has been created it can be passed to any composable which accepts a modifier parameter. Edit the DemoScreen function so that it reads as follows to pass our modifier to the Text composable:

.
.
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
.
.
@Composable
fun DemoScreen() {
 
    val modifier = Modifier
        .border(width = 2.dp, color = Color.Black)
        .padding(all = 10.dp)
        
    Text(
        "Hello Compose", 
        modifier = modifier,
        fontSize = 40.sp,
        fontWeight = FontWeight.Bold
    )
}

When the layout is previewed it should appear as illustrated in Figure 24-1:

Figure 24-1

As we can see from the preview, the padding and border have been applied to the text. Clearly, the Text composable has been implemented such that it accepts a modifier as a parameter. If a composable accepts a modifier it will always be the first optional parameter in the parameter list. This has the added benefit of allowing the modifier to be passed without declaring the argument name. The following, therefore, is syntactically correct:

Text(
    "Hello Compose", 
    modifier,
    fontSize = 40.sp,
    fontWeight = FontWeight.Bold
)

Modifier ordering

The order in which modifiers are chained is of great significance to the resulting output. In the above example, the border was applied first followed by the padding. This has the result of the border appearing outside the padding. To place the border inside the padding, the order of the modifiers needs to be swapped as follows:

val modifier = Modifier
    .padding(all = 10.dp)
    .border(width = 2.dp, color = Color.Black)

When previewed, the Text composable will appear as shown in Figure 24-2 below:

Figure 24-2

If you don’t see the expected effects when working with chained modifiers, keep in mind this may be because of the order in which they are being applied to the component.

Adding modifier support to a composable

So far in this chapter, we have shown how to create a modifier and use it with a built-in composable. When developing your own composables it is important to consider whether modifier support should be included to make the function more configurable.

When adding modifier support to a composable the first rule is that the parameter should be named “modifier” and must be the first optional parameter in the function’s parameter list. As an example, we can add a new composable named CustomImage to our project which accepts as parameters the image resource to display and a modifier. Edit the MainActivity.kt file and add this composable so that it reads as follows:

.
.
import androidx.compose.foundation.Image
import androidx.compose.ui.res.painterResource
.
.
@Composable
fun CustomImage(image: Int) {
    Image(
        painter = painterResource(image),
        contentDescription = null
    )
}

As currently declared, the function only accepts one parameter in the form of the image resource. The next step is to add the modifier parameter:

@Composable
fun CustomImage(image: Int, modifier: Modifier) {
    Image(
        painter = painterResource(image),
        contentDescription = null
    )
}

It is important to remember that the modifier parameter must be optional so that the function can be called without one. This means that we need to specify an empty Modifier instance as the default for the parameter:

@Composable
fun CustomImage(image: Int, modifier: Modifier = Modifier) {
.
.

Finally, we need to make sure that the modifier is applied to the Image composable, keeping in mind that it will be the first optional parameter:

@Composable
fun CustomImage(image: Int, modifier: Modifier = Modifier) {
    Image(
        painter = painterResource(image),
        contentDescription = null,
        modifier
    )
}

Now that we have created a new composable with modifier support we are almost ready to call it from the DemoScreen function. First, however, we need to add an image resource to the project. The image is named vacation.jpg and can be found in the images folder of the sample code archive which can be downloaded from the following web page:

https://www.ebookfrenzy.com/web/compose/index.php

Within Android Studio, display the Resource Manager tool window (View -> Tool Windows -> Resource Manager). Locate the vacation.png image in the file system navigator for your operating system and drag and drop it onto the Resource Manager tool window. In the resulting dialog, click Next followed by the Import button to add the image to the project. The image should now appear in the Resource Manager as shown in Figure 24-3 below:

Figure 24-3

The image will also appear in the res -> drawables section of the Project tool window:

Figure 24-4

Next, modify the DemoScreen composable to include a call to the CustomImage component:

.
.
import androidx.compose.ui.Alignment
import androidx.compose.foundation.layout.*
.
.
@Composable
fun DemoScreen() {
 
    val modifier = Modifier
        .border(width = 2.dp, color = Color.Black)
        .padding(all = 10.dp)
 
    Column(
        Modifier.padding(20.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            "Hello Compose",
            modifier = modifier,
            fontSize = 40.sp,
            fontWeight = FontWeight.Bold
        )
        Spacer(Modifier.height(16.dp))
        CustomImage(R.drawable.vacation)
    }
}

Refresh and build the preview and verify that the layout matches that shown in Figure 24-5 below:

Figure 24-5

At this point, the Image component is using the default Modifier instance that we declared in the CustomImage function signature. To change this we need to construct a custom modifier and pass it through to CustomImage to modify the appearance on the image resource when it is displayed:

.
.
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.draw.clip
.
.
Spacer(Modifier.height(16.dp))
CustomImage(R.drawable.vacation,
    Modifier
        .padding(16.dp)
        .width(270.dp)
        .clip(shape = RoundedCornerShape(30.dp))
)
.
.

The preview should now display the image with padding, fixed width, and rounded corners:

Figure 24-6

Common built-in modifiers

A list of the full set of Modifier methods is beyond the scope of this book (there are currently over 100). For a detailed and complete list of methods, refer to the Compose documentation at the following URL:

https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier

The following is a selection of some of the more commonly used functions:

  • background – Draws a solid colored shape behind the composable.
  • clickable – Specifies a handler to be called when the composable is clicked. Also causes a ripple effect when the click is performed.
  • clip – Clips the composable content to a specified shape.
  • fillMaxHeight – The composable will be sized to fit the maximum height permitted by its parent.
  • fillMaxSize – The composable will be sized to fit the maximum height and width permitted by its parent.
  • fillMaxWidth – The composable will be sized to fit the maximum width permitted by its parent.
  • layout – Used when implementing custom layout behavior, a topic covered in the chapter entitled Building Custom Layouts in Jetpack Compose.
  • offset – Positions the composable the specified distance from its current position along the x and y-axis.
  • padding – Adds space around a composable. Parameters can be used to apply spacing to all four sides or to specify different padding for each side.
  • rotate – Rotates the composable on its center point by a specified number of degrees.
  • scale – Increase or reduce the size of the composable by the specified scale factor.
  • scrollable – Enables scrolling for a composable that extends beyond the viewable area of the layout in which it is contained.
  • size – Used to specify the height and width of a composable. In the absence of a size setting, the composable will be sized to accommodate its content (referred to as wrapping).

Combining modifiers

When working with Compose, situations may arise where you have two or more Modifier objects, all of which need to be applied to the same composable. For this situation, Compose allows modifiers to be combined using the then keyword. The syntax for using this is as follows:

val combinedModifier = firstModifier.then(secondModifier).then(thirdModifier) ...

The result will be a modifier that contains the configurations of all specified modifiers. To see this in action, modify the MainActivity.kt file to add a second modifier for use with the Text composable:

.
.
val modifier = Modifier
    .border(width = 2.dp, color = Color.Black)
    .padding(all = 10.dp)
 
val secondModifier = Modifier.height(100.dp)
.
.

Next, change the Text call to combine both modifiers:

Text(
    "Hello Compose",
    modifier.then(secondModifier),
    fontSize = 40.sp,
    fontWeight = FontWeight.Bold
)

The Text composable should now appear in the preview panel with a height of 100dp in addition to the original font, border, and padding settings.

Summary

Modifiers are created using instances of the Compose Modifier object and are passed as parameters to composables to change appearance and behavior. A modifier is configured by calling methods on the Modifier object to define settings such as size, padding, rotation, and background color. Most of the built-in composables provided with the Compose system will accept a modifier as a parameter. It is also possible (and recommended) to add modifier support to your own composable functions. If a composable function accepts a modifier, it will always be the second optional parameter in the function’s parameter list. Multiple modifier instances may be combined using the then keyword before being applied to a component.

A Jetpack Compose Slot API Tutorial

In this chapter, we will be creating a project within Android Studio to practice the use of slot APIs to build flexible and dynamic composable functions. This will include writing a composable function with two slots and calling that function with different content composables based on selections made by the user.

About the project

Once the project is completed, it will consist of a title, progress indicator, and two checkboxes. The checkboxes will be used to control whether the title is represented as text or graphics, and also whether a circular or linear progress indicator is displayed. Both the title and progress indicator will be declared as slots which will be filled with either a Text or Image composable for the title or, in the case of the progress indicator, a LinearProgressIndicator or CircularProgressIndicator component.

Creating the SlotApiDemo project

Launch Android Studio and select the New Project option from the welcome screen. Within the New Project dialog, choose the Empty Compose Activity template before clicking on the Next button.

Enter SlotApiDemo into the Name field and specify com.example.slotapidemo as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo). Once the project has been created, the SlotApiDemo project should be listed in the Project tool window located along the left-hand edge of the Android Studio main window.

Preparing the MainActivity class file

Android Studio should have automatically loaded the MainActivity.kt file into the code editor. If it has not, locate it in the Project tool window (app -> java -> com.example.slotapidemo -> MainActivity.kt) and double-click on it to load it into the editor. Once loaded, modify the file to remove some template code so that only the following reamins:

package com.example.slotapidemo
.
.
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SlotApiDemoTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

Creating the MainScreen composable

Edit the onCreate method of the MainActivity class to call a composable named MainScreen from within the Surface component:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        SlotDemoTheme {
            Surface(
                modifier = Modifier.fillMaxSize(),
                color = MaterialTheme.colors.background
            ) {
                MainScreen()
            }
        }
    }
}

MainScreen will contain the state and event handlers for the two Checkbox components, so start adding this composable now, making sure to place it after the closing brace (}) of the MainActivity class declaration:

package com.example.slotapidemo
.
.
import androidx.compose.runtime.*
import androidx.compose.material.*
import androidx.compose.foundation.layout.*
.
.
@Composable
fun MainScreen() {
 
    var linearSelected by remember { mutableStateOf(true) }
    var imageSelected by remember { mutableStateOf(true) }
 
    val onLinearClick = { value : Boolean ->
        linearSelected = value
    }
 
    val onTitleClick = { value : Boolean ->
        imageSelected = value
    }
}

Here we have declared two state variables, one for each of the two Checkbox components, and initialized them to true. Next, event handlers have been declared to allow the state of each variable to be changed when the user toggles the Checkbox settings. Later in the project, MainScreen will be modified to call a second composable named ScreenContent.

Adding the ScreenContent composable

When it is called by the MainScreen function, the ScreenContent composable will need to be passed the state variables and event handlers and can initially be declared as follows:

package com.example.slotapidemo
.
.
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
.
.
@Composable
fun ScreenContent(
    linearSelected: Boolean,
    imageSelected: Boolean,
    onTitleClick: (Boolean) -> Unit,
    onLinearClick: (Boolean) -> Unit) {
 
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.SpaceBetween
    ) {
        
    }
}

As the name suggests, the ScreenContent composable is going to be responsible for displaying the screen content including the title, progress indicator, and checkboxes. In preparation for this content, we have made a call to the Column composable and configured it to center its children along the horizontal axis. The SpaceBetween arrangement property has also been set. This tells the column to space its children evenly but not to include spacing before the first or after the last child.

One of the child composables which will be called by ScreenContent will be responsible for rendering the two Checkbox components. While these could be added directly within the Column composable, a better approach is to place them in a separate composable which can be called from within ScreenContent.

Creating the Checkbox composable

The composable containing the checkboxes will consist of a Row component containing two Checkbox instances. In addition, Text composables will be positioned to the left of each Checkbox with a Spacer separating the two Text/Checkbox pairs.

When it is called, the Checkboxes composable will need to be passed the two state variables which will be used to make sure the checkboxes display the current state. Also passed will be references to the onLinearClick and onTitleClick event handlers which will be assigned to the onCheckChange properties of the two Checkbox components.

Remaining within the MainActivity.kt file, add the CheckBoxes composable so that it reads as follows:

.
.
import androidx.compose.foundation.layout.Row
.
.
@Composable
fun CheckBoxes(
    linearSelected: Boolean,
    imageSelected: Boolean,
    onTitleClick: (Boolean) -> Unit,
    onLinearClick: (Boolean) -> Unit
) {
    Row(Modifier.padding(20.dp)) {
 
        Checkbox(
            checked = imageSelected,
            onCheckedChange = onTitleClick
        )
        Text("Image Title")
        Spacer(Modifier.width(20.dp))
        Checkbox(checked = linearSelected,
            onCheckedChange = onLinearClick
        )
        Text("Linear Progress")
    }
}

If you would like to preview the composable before proceeding, add the following preview declaration before clicking on the Build & Refresh link in the Preview panel:

@Preview
@Composable
fun DemoPreview() {
    CheckBoxes(
        linearSelected = true, 
        imageSelected = true, 
        onTitleClick = { /*TODO*/ }, 
        onLinearClick = { /*TODO*/})
}

When calling the CheckBoxes composable in the above preview function we are setting the two state properties to true and assigning stub lambdas that do nothing as the event callbacks.

Once the preview has been refreshed, the layout should match that shown in Figure 23-1 below:

Figure 23-1

Implementing the ScreenContent slot API

Now that we have added the composable containing the two checkboxes, we can call it from within the Column contained within ScreenContent. Since both the state variables and event handlers were already passed into ScreenContent, we can simply pass these to the Checkboxes composable when we call it. Locate the ScreenContent composable and modify it as follows:

@Composable
fun ScreenContent(
    linearSelected: Boolean,
    imageSelected: Boolean,
    onTitleClick: (Boolean) -> Unit,
    onLinearClick: (Boolean) -> Unit) {
 
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.SpaceBetween
    ) {
        CheckBoxes(linearSelected, imageSelected, onTitleClick, onLinearClick)
    }
}

In addition to the row of checkboxes, ScreenContent also needs slots for the title and progress indicator. These will be named titleContent and progressContent and need to be added as parameters and referenced as children of the Column:

@Composable
fun ScreenContent(
    linearSelected: Boolean,
    imageSelected: Boolean,
    onTitleClick: (Boolean) -> Unit,
    onLinearClick: (Boolean) -> Unit,
    titleContent: @Composable () -> Unit,
    progressContent: @Composable () -> Unit) {
 
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.SpaceBetween
    ) {
        titleContent()
        progressContent()
        CheckBoxes(linearSelected, imageSelected, onTitleClick, onLinearClick)
    }
}

All that remains is to add some code to the MainScreen declaration so that different composables are provided for the slots based on the current values of the linearSelected and imageSelected state variables. Before taking that step, however, we need to add one more composable to display an image in the title slot.

Adding an Image drawable resource

For this example, we will use one of the built-in vector drawings included with the Android SDK. To select a drawing and add it to the project, begin by locating the drawable folder in the Project tool window (app -> res -> drawable) and right-click on it. In the resulting menu (Figure 23-2) select the New -> Vector Asset menu option:

Figure 23-2

Once the menu option has been selected, Android Studio will display the Asset Studio dialog shown in Figure 23-3 below:

Figure 23-3

Within the dialog, click on the image to the right of the Clip Art label as indicated by the arrow in the above figure to display a list of available icons. In the search box, enter “cloud” and select the “Cloud Download” icon as shown in Figure 23-4 below:

Figure 23-4

Click on the OK button to select the drawing and return to the Asset Studio dialog. Increase the size of the image to 150dp x 150dp before clicking the Next button. On the subsequent screen, click on Finish to save the file in the default location.

While it was possible to change the color of the image in the Asset Studio dialog, the color selector only allows us to specify colors by RGB value. Instead, we want to use a named color from the project theme. Within the Project tool window, find and open the Theme.kt file located under app -> com.example.slotapidemo -> ui.theme. This file contains color settings for both light and dark color palettes. In this example, the plan is to use the primaryVariant color setting which, in both palettes, is set to a color named Purple700:

primaryVariant = Purple700

Having chosen a color from the theme, double-click on the ic_baseline_cloud_download_24.xml vector asset file in the Project tool window to load it into the code editor and modify the android:tint property as follows:

<vector android:height="150dp" android:tint="@color/purple_700"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="150dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM17,13l-5,5 -5,-5h3V9h4v4h3z"/>
</vector>

Writing the TitleImage composable

Now that we have an image to display for the title, the next step is to write a composable to display the image. To make this composable as reusable as possible we will design it so that it is passed the image resource to be displayed:

.
.
import androidx.compose.foundation.Image
import androidx.compose.ui.res.painterResource
.
.
@Composable
fun TitleImage(drawing: Int) {
    Image(
        painter = painterResource(drawing),
        contentDescription = "title image"
    )
}

The Image component provides several ways to render graphics depending on which parameters are used when it is called. Since we are using a resource image, the component makes a call to the painterResource method to render the image.

Completing the MainScreen composable

Now that all of the child composables have been added and the state variable and event handlers implemented, it is time to complete work on the MainScreen declaration. Specifically, code needs to be added to this composable so that different content is displayed in the two ScreenContent slots depending on the current checkbox selections.

Locate the MainScreen composable in the MainActivity.kt file and add code to call the ScreenContent function as follows:

@Composable
fun MainScreen() {
 
    var linearSelected by remember { mutableStateOf(true) }
    var imageSelected by remember { mutableStateOf(true) }
 
    val onLinearClick = { value : Boolean ->
        linearSelected = value
    }
 
    val onTitleClick = { value : Boolean ->
        imageSelected = value
    }
 
    ScreenContent(
        linearSelected = linearSelected,
        imageSelected = imageSelected,
        onLinearClick = onLinearClick,
        onTitleClick = onTitleClick,
        titleContent = {
            if (imageSelected) {
 
                TitleImage(drawing = R.drawable.ic_baseline_cloud_download_24)
 
            } else {
                Text("Downloading", style = MaterialTheme.typography.h3,
                    modifier = Modifier.padding(30.dp))
            }
        },
        progressContent = {
            if (linearSelected) {
                LinearProgressIndicator(Modifier.height(40.dp))
            } else {
                CircularProgressIndicator(Modifier.size(200.dp), 
                      strokeWidth = 18.dp)
            }
        }
    )
}

The ScreenContent call begins by passing through the state variables and event handlers which will subsequently be passed down to the two Checkbox instances:

ScreenContent(
    linearSelected = linearSelected,
    imageSelected = imageSelected,
    onLinearClick = onLinearClick,
    onTitleClick = onTitleClick,

The next parameter deals with the titleContent slot and uses an if statement to pass through either a TitleImage or Text component depending on the current value of the imageSelected state:

titleContent = {
    if (imageSelected) {
 
        TitleImage(drawing = R.drawable.ic_baseline_cloud_download_24)
 
    } else {
        Text("Downloading", style = MaterialTheme.typography.h3,
            modifier = Modifier.padding(30.dp))
    }
},

Finally, either a linear or circular progress indicator is used to fill ScreenContent’s progressContent slot based on the current value of the linearSelected state:

progressContent = {
    if (linearSelected) {
        LinearProgressIndicator(Modifier.height(40.dp))
    } else {
        CircularProgressIndicator(Modifier.size(200.dp), strokeWidth = 18.dp)
    }
}

Note that we haven’t passed a progress value through to either of the progress indicators. This will cause the components to enter indeterminate progress mode which will cause them to show a continually cycling indicator.

Previewing the project

With these changes complete, the project is now ready to preview. Locate the DemoPreview composable added earlier in the chapter and modify it so that it calls MainScreen instead of the Checkboxes composable and to add the system UI to the preview:

@Preview(showSystemUi = true)
@Composable
fun DemoPreview() {
    MainScreen()
}

Once a rebuild has been performed, the Preview panel should resemble that shown in Figure 23-5:

Figure 23-5

To test that the project works, start interactive mode by clicking on the button indicated in Figure 23-6:

Figure 23-6

Once interactive mode has started, experiment with different combinations of checkbox settings to confirm that the slot API for the ScreenContent composable is performing as expected. Figure 23-7, for example, shows the rendering with both checkboxes disabled:

Figure 23-7

Summary

In this chapter, we have demonstrated the use of a slot API to insert different content into a composable at the point that it is called during runtime. Incidentally, we also passed state variables and event handler references down through multiple levels of composable functions and explored how to use Android Studio’s Asset Studio to select and configure built-in vector drawable assets. Finally, we also made use of the built-in Image component to render an image within a user interface layout.

An Overview of Jetpack Compose Slot APIs

Now that we have a better idea of what composable functions are and how to create them, it is time to explore composables that provide a slot API. In this chapter, we will explain what a slot API is, what it is used for and how you can include slots in your own composable functions. We will also explore some of the built-in composables that provide slot API support.

Understanding slot APIs

As we already know, composable functions can include calls to one or more other composable functions. This usually means that the content of a composable is predefined in terms of which other composables it calls and, therefore, the content it displays. Consider the following function consisting of a Column and three Text components:

@Composable
fun SlotDemo() {
    Column {
        Text("Top Text")
        Text("Middle Text")
        Text("Bottom Text")
    }
}

The function could be modified to pass in parameters that specify the text to be displayed or even the color and font size of that text. Regardless of the changes we make, however, the function is still restricted to displaying a column containing three Text components:

Figure 22-1

Suppose, however, that we need to display three items in a column, but do not know what composable will take up the middle position until just before the composable is called. In its current form, there is no way to display anything but the declared Text component in the middle position. The solution to this problem is to open up the middle composable as a slot into which any other composable may be placed when the function is called. This is referred to as providing a slot API for the composable. API is an abbreviation of Application Programming Interface and, in this context, implies that we are adding a programming interface to our composable that allows the caller to specify the composable to appear within a slot. In fact, a composable function can provide multiple slots to the caller. In the above function, for example, all of the Text components could be declared as slots if required.

Declaring a slot API

It can be helpful to think of a slot API composable as a user interface template in which one or more elements are left blank. These missing pieces are then passed as parameters when the composable is called and included when the user interface is rendered by the Compose runtime system.

The first step in adding slots to a composable is to specify that it accepts a slot as a parameter. This is essentially a case of declaring that a composable accepts other composables as parameters. In the case of our example SlotDemo composable, we would modify the function signature as follows:

@Composable
fun SlotDemo(middleContent: @Composable () -> Unit) {
.
.

When the SlotDemo composable is called, it will now need to be passed a composable function. Note that the function is declared as returning a Unit object. Unit is a Kotlin type used to indicate that a function does not return any value. Unit can be considered to be the Kotlin equivalent of void in other languages. The parameter has been assigned a label of “middleContent”, though this could be any valid label name that helps to describe the slot and allows us to reference it within the body of the function.

The only remaining change to this composable is to substitute the middleContent component into the Column declaration as follows:

@Composable
fun SlotDemo(middleContent: @Composable () -> Unit) {
    Column {
        Text("Top Text")
        middleContent()
        Text("Bottom Text")
    }
}

We have now successfully declared a slot API for our SlotDemo composable.

Calling slot API composables

The next step is to learn how to make use of the slot API configured into our SlotDemo composable. This simply involves passing a composable through as a parameter when making the SlotDemo function call. Suppose, for example, that we need the following composable to appear in the middleContent slot:

@Composable
fun ButtonDemo() {
    Button(onClick = { }) {
        Text("Click Me")
    }
}

We can now call our SlotDemo composable function as follows:

SlotDemo(middleContent = { ButtonDemo() })

While this syntax works, it can quickly become cluttered if the composable has more than one slot to be filled. A cleaner syntax reads as follows:

SlotDemo { 
    ButtonDemo() 
}

Regardless of the syntax used, the design will be rendered as shown below in Figure 22-2:

Figure 22-2

A slot API is not, of course, limited to a single slot. The SlotDemo example could be composed entirely of slots as follows:

@Composable
fun SlotDemo(
    topContent: @Composable () -> Unit,
    middleContent: @Composable () -> Unit,
    bottomContent: @Composable () -> Unit) {
    Column {
        topContent()
        middleContent()
        bottomContent()
    }
}

With these changes made, the call to SlotDemo could be structured as follows:

SlotDemo(
    topContent = { Text("Top Text") },
    middleContent = { ButtonDemo() },
    bottomContent = { Text("Bottom Text") }
)

As with the single slot, this can be abbreviated for clarity:

SlotDemo(
    { Text("Top Text") },
    { ButtonDemo() },
    { Text("Bottom Text") }
)

Summary

In this chapter, we have introduced the concept of slot APIs and demonstrated how they can be added to composable functions. By implementing a slot API, the content of a composable function can be specified dynamically at the point that it is called. This contrasts with the static content of a typical composable where the content is defined at the point the function is written and cannot subsequently be changed. A composable with a slot API is essentially a user interface template containing one or more slots into which other composables can be inserted at runtime.

With the basics of slot APIs covered in this chapter, the next chapter will create a project that puts this theory into practice.

A Jetpack Compose Composition Local Tutorial

We already know from previous chapters that user interfaces are built in Compose by constructing hierarchies of composable functions. We also know that Compose is state-driven and that state should generally be declared in the highest possible node of the composable tree (a concept referred to as state hoisting) and passed down through the hierarchy to the descendant composables where it is needed. While this works well for most situations, it can become cumbersome if the state needs to be passed down through multiple levels within the hierarchy. A solution to this problem exists in the form of CompositionLocal, which is the subject of this chapter.

Understanding CompositionLocal

In simple terms, CompositionLocal provides a way to make state declared higher in the composable hierarchy tree available to functions lower in the tree without having to pass it through every composable between the point where it is declared and the function where it is used. Consider, for example, the following hierarchy diagram:

Figure 21-1

In the hierarchy, a state named colorState is declared in Composable1 but is only used in Composable8. Although the state is not needed in either Composable3 or Composable5, colorState still needs to be passed down through those functions to reach Composable8. The deeper the tree becomes, the more levels through which the state needs to be passed to reach the function where it is used.

A solution to this problem is to use CompositionLocal. CompositionLocal allows us to declare the data at the highest necessary node in the tree and then access it in descendants without having to pass it through the intervening children as shown in Figure 21-2:

Figure 21-2

CompositionLocal has the added advantage of only making the data available to the branch of the tree below the point at which it is assigned a value. In other words, if the state were assigned a value when calling composable3 it will be accessible within composable numbers 3, 5, 7, and 8, but not to composables 1, 2, 4, or 6. This allows state to be kept local to specific branches of the composable tree, and also for different sub-branches to have different values assigned to the same CompositionLocal state. Composable5 could, for example, have a different color assigned to colorState from that assigned when Composable7 is called.

Using CompositionLocal

Declaring state using CompositionLocal starts with the creation of a ProvidableCompositionLocal instance which can be obtained via a call to either the compositionLocalOf() or staticCompositionLocalOf() function. In each case the function accepts a lambda defining a default value to be assigned to the state in the absence of a specific assignment, for example:

val LocalColor = compositionLocalOf { Color.Red } 
val LocalColor = staticCompositionLocalOf { Color.Red }

The staticCompositionLocalOf() function is recommended for storing state values that are unlikely to change very often. The reason for this is that any changes to the state value will cause the entire tree beneath where the value is assigned to be recomposed. The compositionLocalOf() function, on the other hand, will only cause recomposition to be performed on composables where the current state is accessed. This function should be used when dealing with states that change frequently.

The next step is to assign a value to the ProvidableCompositionLocal instance and wrap the call to the immediate descendant child composable in a CompositionLocalProvider call:

val color = Color.Blue
 
CompositionLocalProvider(LocalColor provides color) {
    Composable5()
}

Any descendants of Composition5 will now be able to access the CompositionLocal state via the current property of the ProviderCompositionLocal instance, for example:

val background = LocalColor.current

In the rest of this chapter, we will build a project that mirrors the hierarchy illustrated in Figure 21-1 to show CompositionLocal in action.

Creating the CompLocalDemo project

Launch Android Studio and create a new Empty Compose Activity project named CompLocalDemo, specifying com.example.complocaldemo as the package name, and selecting a minimum API level of API 26: Android 8.0 (Oreo). Within the MainActivity.kt file, delete the Greeting function and add a new empty composable named Composable1:

@Composable
fun Composable1() {
    
}

Next, edit the onCreateActivity() method and DefaultPreview function to call Composable1 instead of Greeting.

Designing the layout

Within the MainActivity.kt file, implement the composable hierarchy as follows:

.
.
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
.
.
@Composable
fun Composable1() {
    Column {
        Composable2()
        Composable3()
    }
}
 
@Composable
fun Composable2() {
    Composable4()
}
 
@Composable
fun Composable3() {
    Composable5()
}
 
@Composable
fun Composable4() {
    Composable6()
}
 
@Composable
fun Composable5() {
    Composable7()
    Composable8()
}
 
@Composable
fun Composable6() {
    Text("Composable 6")
}
 
@Composable
fun Composable7() {
 
}
 
@Composable
fun Composable8() {
    Text("Composable 8")
}

Adding the CompositionLocal state

The objective for this project is to declare a color state that can be changed depending on whether the device is in light or dark mode, and use that to control the background color of the text component in Composable8. Since this is a value that will not change regularly, we can use the staticCompositionLocalOf() function. Remaining within the MainActivity.kt file, add the following line above the Composable1 declaration:

.
.
val LocalColor = staticCompositionLocalOf { Color(0xFFffdbcf) }
 
@Composable
fun Composable1() {
    Column {
.
.

Next, a call to isSystemInDarkTheme() needs to be added, and the result used to assign a different color to the LocalColor state. We also need to call Composable3 from within the context of the CompositionLocal provider:

@Composable
fun Composable1() {
 
    var color = if (isSystemInDarkTheme()) {
        Color(0xFFa08d87)
    } else {
        Color(0xFFffdbcf)
    }
 
    Column {
        Composable2()
 
        CompositionLocalProvider(LocalColor provides color) {
            Composable3()
        }      
    }
}

Accessing the CompositionLocal state

The final task before testing the code is to assign the color state to the Text component in Composable8 as follows:

@Composable
fun Composable8() {
    Text("Composable 8", modifier = Modifier.background(LocalColor.current))
}

Testing the design

To test the activity code in both light and dark modes, add a new Preview composable to MainActivity.kt with uiMode set to UI_NIGHT_MODE_YES:

.
.
import android.content.res.Configuration.UI_MODE_NIGHT_YES
.
.
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
@Composable
fun DarkPreview() {
    CompLocalDemoTheme {
        Composable1()
    }
}

After refreshing the Preview panel, both the default and dark preview should appear, each using a different color as the background for the Text component in Composable8:

Figure 21-3

We can also modify the code so that composables 3, 5, 7, and 8 all have a different color setting. All this requires is calling each composable from within a CompositionLocalProvider with a different color assignment:

.
.
@Composable
fun Composable3() {
 
    Text("Composable 3", modifier = Modifier.background(LocalColor.current))
 
    CompositionLocalProvider(LocalColor provides Color.Red) {
        Composable5()
    }
}
.
.
@Composable
fun Composable5() {
 
    Text("Composable 5", modifier = Modifier.background(LocalColor.current))
 
    CompositionLocalProvider(LocalColor provides Color.Green) {
        Composable7()
    }
 
    CompositionLocalProvider(LocalColor provides Color.Yellow) {
        Composable8()
    }
}
.
.
@Composable
fun Composable7() {
    Text("Composable 7", modifier = Modifier.background(LocalColor.current))
}
.
.

Now when the Preview panel is refreshed, all four components will have a different color, all based on the same LocalColor state:

Figure 21-4

As one final step, try to access the LocalColor state from Composable6:

@Composable
fun Composable6() {
    Text("Composable 6", modifier = Modifier.background(LocalColor.current))
}

On refreshing the preview the Text component for Compsoable6 will appear using the default color assigned to LocalColor. This is because Composable6 is in a different branch of the tree and does not have access to the current LocalColor setting.

Summary

This chapter has introduced CompositionLocal and demonstrated how it can be used to declare state that is accessible to composables lower down in the layout hierarchy without having to be passed from one child to another. State declared in this way is local to the branch of the hierarchy tree in which a value is assigned.

Jetpack Compose State and Recomposition

State is the cornerstone of how the Compose system is implemented. As such, a clear understanding of state is an essential step in becoming a proficient Compose developer. In this chapter, we will explore and demonstrate the basic concepts of state and explain the meaning of related terms such as recomposition, unidirectional data flow, and state hoisting. The chapter will also cover saving and restoring state through configuration changes.

The basics of state

In declarative languages such as Compose, state is generally referred to as “a value that can change over time”. At first glance, this sounds much like any other data in an app. A standard Kotlin variable, for example, is by definition designed to store a value that can change at any time during execution. State, however, differs from a standard variable in two significant ways.

First, the value assigned to a state variable in a composable function needs to be remembered. In other words, each time a composable function containing state (a stateful function) is called, it must remember any state values from the last time it was invoked. This is different from a standard variable which would be re-initialized each time a call is made to the function in which it is declared.

The second key difference is that a change to any state variable has far reaching implications for the entire hierarchy tree of composable functions that make up a user interface. To understand why this is the case, we now need to talk about recomposition.

Introducing recomposition

When developing with Compose, we build apps by creating hierarchies of composable functions. As previously discussed, a composable function can be thought of as taking data and using that data to generate sections of a user interface layout. These elements are then rendered on the screen by the Compose runtime system. In most cases, the data passed from one composable function to another will have been declared as a state variable in a parent function. This means that any change of state value in a parent composable will need to be reflected in any child composables to which the state has been passed. Compose addresses this by performing an operation referred to as recomposition.

Recomposition occurs whenever a state value changes within a hierarchy of composable functions. As soon as Compose detects a state change, it works through all of the composable functions in the activity and recomposes any functions affected by the state value change. Recomposing simply means that the function gets called again and passed the new state value.

Recomposing the entire composable tree for a user interface each time a state value changes would be a highly inefficient approach to rendering and updating a user interface. Compose avoids this overhead using a technique called intelligent recomposition that involves only recomposing those functions directly affected by the state change. In other words, only functions that read the state value will be recomposed when the value changes.

Creating the StateExample project

Launch Android Studio and select the New Project option from the welcome screen. Within the resulting new project dialog, choose the Empty Compose Activity template before clicking on the Next button.

Enter StateExample into the Name field and specify com.example.stateexample as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo). On completion of the project creation process, the StateExample project should be listed in the Project tool window located along the left-hand edge of the Android Studio main window.

Declaring state in a composable

The first step in declaring a state value is to wrap it in a MutableState object. MutableState is a Compose class which is referred to as an observable type. Any function that reads a state value is said to have subscribed to that observable state. As a result, any changes to the state value will trigger the recomposition of all subscribed functions.

Within Android Studio, open the MainActivity.kt file, delete the Greeting composable and modify the class so that it reads as follows:

package com.example.stateexample
.
.
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            StateExampleTheme {
                Surface(color = MaterialTheme.colors.background) {
                    DemoScreen()
                }
            }
        }
    }
}
 
@Composable
fun DemoScreen() {
    MyTextField()
}
 
@Composable
fun MyTextField() {
 
}
 
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    StateExampleTheme {
        DemoScreen()
    }
}

The objective here is to implement MyTextField as a stateful composable function containing a state variable and an event handler that changes the state based on the user’s keyboard input. The result is a text field in which the characters appear as they are typed.

MutableState instances are created by making a call to the mutableStateOf() runtime function, passing through the initial state value. The following, for example, creates a MutableState instance initialized with an empty String value:

var textState = { mutableStateOf("") }

This provides an observable state which will trigger a recomposition of all subscribed functions when the contained value is changed. The above declaration is, however, missing a key element. As previously discussed, state must be remembered through recompositions. As currently implemented, the state will be reinitialized to an empty string each time the function in which it is declared is recomposed. To retain the current state value, we need to use the remember keyword:

var myState = remember { mutableStateOf("") }

Remaining within the MainActivity.kt file, add some imports and modify the MyTextField composable as follows:

.
.
import androidx.compose.material.*
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.foundation.layout.Column
.
.
@Composable
fun MyTextField() {
 
    var textState = remember { mutableStateOf("") }
 
    val onTextChange = { text : String ->
        textState.value = text
    }
 
    TextField(
        value = textState.value,
        onValueChange = onTextChange
    )
}

Test the code using the Preview panel in interactive mode and confirm that keyboard input appears in the TextField as it is typed. Note that at the time of writing, keyboard input within the preview was not working. If you encounter a similar problem, run the app on an emulator or physical device to test.

When looking at Compose code examples, you may see MutableState objects declared in different ways. When using the above format, it is necessary to read and set the value property of the MutableState instance. For example, the event handler to update the state reads as follows:

val onTextChange = { text: String ->
     textState.value = text
}

Similarly, the current state value is assigned to the TextField as follows:

TextField(
    value = textState.value,
    onValueChange = onValueChange
)

A more common and concise approach to declaring state is to use Kotlin property delegates via the by keyword as follows (note that two additional libraries need to be imported when using property delegates):

.
.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
.
.
@Composable
fun MyTextField() {
 
    var textState by remember { mutableStateOf("") }
.
.

We can now access the state value without needing to directly reference the MutableState value property within the event handler:

val onTextChange = { text: String ->
     textState = text
}

This also makes reading the current value more concise:

TextField(
    value = textState,
    onValueChange = onTextChange
 )

A third technique separates the access to a MutableState object into a value and a setter function as follows:

var (textValue, setText) = remember { mutableStateOf("") }

When changing the value assigned to the state we now do so by calling the setText setter, passing through the new value:

val onTextChange = { text: String ->
     setText(text)
}

The state value is now accessed by referencing textValue:

TextField(
    value = textValue,
    onValueChange = onValueChange
)

In most cases, the use of the by keyword and property delegates is the most commonly used technique because it results in cleaner code. Before continuing with the chapter, revert the example to use the by keyword.

Unidirectional data flow

Unidirectional data flow is an approach to app development whereby state stored in a composable should not be directly changed by any child composable functions. Consider, for example, a composable function named FunctionA containing a state value in the form of a Boolean value. This composable calls another composable function named FunctionB that contains a Switch component. The objective is for the switch to update the state value each time the switch position is changed by the user. In this situation, adherence to unidirectional data flow prohibits FunctionB from directly changing the state value.

Instead, FunctionA would declare an event handler (typically in the form of a lambda) and pass it as a parameter to the child composable along with the state value. The Switch within FunctionB would then be configured to call the event handler each time the switch position changes, passing it the current setting value. The event handler in FunctionA will then update the state with the new value.

Make the following changes to the MainActivity.kt file to implement FunctionA and FunctionB together with a corresponding modification to the preview composable:

@Composable
fun FunctionA() {
 
    var switchState by remember { mutableStateOf(true) }
 
    val onSwitchChange = { value : Boolean ->
        switchState = value
    }
    
    FunctionB(
        switchState = switchState,
        onSwitchChange = onSwitchChange
    )
}
 
@Composable
fun FunctionB(switchState: Boolean, onSwitchChange : (Boolean) -> Unit ) {
    Switch(
        checked = switchState,
        onCheckedChange = onSwitchChange
    )
}
 
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    StateExampleTheme {
        Column {
            DemoScreen()
            FunctionA()
        }
    }
}

Preview the app using interactive mode and verify that clicking the switch changes the slider position between on and off states.

We can now use this example to break down the state process into the following individual steps which occur when FunctionA is called:

  1. The switchState state variable is initialized with a true value.
  2. The onSwitchChange event handler is declared to accept a Boolean parameter which it assigns to switchState when called.
  3. FunctionB is called and passed both switchState and a reference to the onSwitchChange event handler.
  4. FunctionB calls the built-in Switch component and configures it to display the state assigned to switchState. The Switch component is also configured to call the onSwitchChange event handler when the user changes the switch setting.
  5. Compose renders the Switch component on the screen.

The above sequence explains how the Switch component gets rendered on the screen when the app first launches.

We can now explore the sequence of events that occur when the user slides the switch to the “off” position:

  1. The switch is moved to the “off” position.
  2. The Switch component calls the onSwitchChange event handler passing through the current switch position value (in this case false).
  3. The onSwitchChange lambda declared in FunctionA assigns the new value to switchState.
  4. Compose detects that the switchState state value has changed and initiates a recomposition.
  5. Compose identifies that FunctionB contains code that reads the value of switchState and therefore needs to be recomposed.
  6. Compose calls FunctionB with the latest state value and the reference to the event handler.
  7. FunctionB calls the Switch composable and configures it with the state and event handler.
  8. Compose renders the Switch on the screen, this time with the switch in the “off” position.

The key point to note about this process is that the value assigned to switchState is only changed from within FunctionA and never directly updated by FunctionB. The Switch setting is not moved from the “on” position to the “off” position directly by FunctionB. Instead, the state is changed by calling upwards to the event handler located in FunctionA, and allowing recomposition to regenerate the Switch with the new position setting.

As a general rule, data is passed down through a composable hierarchy tree while events are called upwards to handlers in ancestor components as illustrated in Figure 20-1:

Figure 20-1

State hoisting

If you look up the word “hoist” in a dictionary it will likely be defined as the act of raising or lifting something. The term state hoisting has a similar meaning in that it involves moving state from a child composable up to the calling (parent) composable or a higher ancestor. When the child composable is called by the parent, it is passed the state along with an event handler. When an event occurs in the child composable that requires an update to the state, a call is made to the event handler passing through the new value as outlined earlier in the chapter. This has the advantage of making the child composable stateless and, therefore, easier to reuse. It also allows the state to be passed down to other child composables later in the app development process. Consider our MyTextField example from earlier in the chapter:

@Composable
fun DemoScreen() {
    MyTextField()
}
 
@Composable
fun MyTextField() {
 
    var textState by remember { mutableStateOf("") }
 
    val onTextChange = { text : String ->
        textState = text
    }
 
    TextField(
        value = textState,
        onValueChange = onTextChange
    )
}

The self-contained nature of the MyTextField composable means that it is not a particularly useful component. One issue is that the text entered by the user is not accessible to the calling function and, therefore, cannot be passed to any sibling functions. It is also not possible to pass a different state and event handler through to the function, thereby limiting its re-usability.

To make the function more useful we need to hoist the state into the parent DemoScreen function as follows:

@Composable
fun DemoScreen() {
 
    var textState by remember { mutableStateOf("") }
 
    val onTextChange = { text : String ->
        textState = text
    }
 
    MyTextField(text = textState, onTextChange = onTextChange)
}
 
@Composable
fun MyTextField(text: String, onTextChange : (String) -> Unit) {
 
    TextField(
        value = text,
        onValueChange = onTextChange
    )
}

With the state hoisted to the parent function, MyTextField is now a stateless, reusable composable which can be called and passed any state and event handler. Also, the text entered by the user is now accessible by the parent function and may be passed down to other composables if necessary.

State hoisting is not limited to moving to the immediate parent of a composable. State can be raised any number of levels upward within the composable hierarchy and subsequently passed down through as many layers of children as needed (within reason). This will often be necessary when multiple children need access to the same state. In such a situation, the state will need to be hoisted up to an ancestor that is common to both children.

In Figure 20-2 below, for example, both NameField and NameText need access to textState. The only way to make the state available to both composables is to hoist it up to the MainScreen function since this is the only ancestor both composables have in common:

Figure 20-2

The solid arrows indicate the path of textState as it is passed down through the hierarchy to the NameField and NameText functions (in the case of the NameField, a reference to the event handler is also passed down), while the dotted line represents the calls from NameField function to an event handler declared in MainScreen as the text changes.

Note that if you find yourself passing state down through an excessive number of child layers, it may be worth looking at CompositionLocalProvider, a topic covered in the chapter entitled An Introduction to Composition Local.

When adding state to a function, take some time to decide whether hoisting state to the caller (or higher) might make for a more re-usable and flexible composable. While situations will arise where state is only needed to be used locally in a composable, in most cases it probably makes sense to hoist the state up to an ancestor.

Saving state through configuration changes

We now know that the remember keyword can be used to save state values through recompositions. This technique does not, however, retain state between configuration changes. A configuration change generally occurs when some aspect of the device changes in a way that alters the appearance of an activity (such as rotating the orientation of the device between portrait and landscape or changing a system-wide font setting).

Changes such as these will cause the entire activity to be destroyed and recreated. The reasoning behind this is that such changes affect resources such as the layout of the user interface and simply destroying and recreating impacted activities is the quickest way for an activity to respond to the configuration change. The result is a newly initialized activity with no memory of any previous state values.

To experience the effect of a configuration change, run the StateExample app on an emulator or device and, once running, enter some text so that it appears in the TextField before changing the orientation from portrait to landscape. When using the emulator, device rotation may be simulated using the rotation button located in the emulator toolbar. To complete the rotation on Android 11 or older, it may also be necessary to tap on the rotation button. This appears in the toolbar of the device or emulator screen as shown in Figure 20-3:

Figure 20-3

Before performing the rotation on Android 12 or later, you may need to enter the Settings app, select the Display category and enable the Auto-rotate screen option.

Note that after rotation, the TextField is now blank and the text entered has been lost. In situations where state needs to be retained through configuration changes, Compose provides the rememberSaveable keyword. When rememberSaveable is used, the state will be retained not only through recompositions, but also configuration changes. Modify the textState declaration to use rememberSaveable as follows:

.
.
import androidx.compose.runtime.saveable.rememberSaveable
.
.
@Composable
fun DemoScreen() {
 
    var textState by rememberSaveable { mutableStateOf("") }
.
.

Build and run the app once again, enter some text and perform another rotation. Note that the text is now preserved following the configuration change.

Summary

When developing apps with Compose it is vital to have a clear understanding of how state and recomposition work together to make sure that the user interface is always up to date. In this chapter, we have explored state and described how state values are declared, updated, and passed between composable functions. You should also have a better understanding of recomposition and how it is triggered in response to state changes.

We also introduced the concept of unidirectional data flow and explained how data flows down through the compose hierarchy while data changes are made by making calls upward to event handlers declared within ancestor stateful functions.

An important goal when writing composable functions is to maximize reusability. This can be achieved, in part, by hoisting state out of a composable up to the calling parent or a higher function in the compose hierarchy.

Finally, the chapter described configuration changes and explained how such changes result in the destruction and recreation of entire activities. Ordinarily, state is not retained through configuration changes unless specifically configured to do so using the rememberSaveable keyword.

Jetpack Composable Functions Overview

Composable functions are the building blocks used to create user interfaces for Android apps when developing with Jetpack Compose. In the ComposeDemo project created earlier in the book, we made use of both the built-in compose functions provided with Compose and also created our own functions. In this chapter, we will explore composable functions in more detail, including topics such as stateful and stateless functions, function syntax, and the difference between foundation and material composables.

What is a composable function?

Composable functions (also referred to as composables or components) are special Kotlin functions that are used to create user interfaces when working with Compose. A composable function is differentiated from regular Kotlin functions in code using the @Composable annotation.

When a composable is called, it is typically passed some data and a set of properties that define how the corresponding section of the user interface is to behave and appear when rendered to the user in the running app. In essence, composable functions transform data into user interface elements. Composables do not return values in the traditional sense of the Kotlin function, but instead, emit user interface elements to the Compose runtime system for rendering.

Composable functions can call other composables to create a hierarchy of components as demonstrated in the ComposeDemo project. While a composable function may also call standard Kotlin functions, standard functions may not call composable functions.

A typical Compose-based user interface will be comprised of a combination of built-in and custom-built composables.

Stateful vs. stateless composables

Composable functions are categorized as being either stateful or stateless. State, in the context of Compose, is defined as being any value that can change during the execution of an app. For example, a slider position value, the string entered into a text field, or the current setting of a check box are all forms of state.

As we saw in the ComposeDemo project, a composable function can store a state value which defines in some way how the composable function, or those that it calls appear or behave. This is achieved using the remember keyword and the mutableStateOf function. Our DemoScreen composable, for example, stored the current slider position as state using this technique:

@Composable
fun DemoScreen() {
 
    var sliderPosition by remember { mutableStateOf(20f) }
.
.
}

Because the DemoScreen contains state, it is considered to be a stateful composable. Now consider the DemoSlider composable which reads as follows:

@Composable
fun DemoSlider(sliderPosition: Float, onPositionChange : (Float) -> Unit ) {
    Slider(
        modifier = Modifier.padding(10.dp),
        valueRange = 20f..40f,
        value = sliderPosition,
        onValueChange = onPositionChange
    )
}

Although this composable is passed and makes use of the state value stored by the DemoScreen, it does not itself store any state value. DemoSlider is, therefore, considered to be a stateless composable function.

The topic of state will be covered in greater detail in the chapter entitled An Overview of Compose State and Recomposition.

Composable function syntax

Composable functions, as we already know, are declared using the @Composable annotation and are written in much the same way as a standard Kotlin function. We can, for example, declare a composable function that does nothing as follows:

@Composable
fun MyFunction() {
}

We can also call other composables from within the function:

@Composable
fun MyFunction() {
    Text("Hello")
}

Composables may also be implemented to accept parameters. The following function accepts text, font weight, and color parameters and passes them to the built-in Text composable. The fragment also includes a preview composable to demonstrate how the CustomText function might be called:

@Composable
fun CustomText(text: String, fontWeight: FontWeight, color: Color) {
    Text(text = text, fontWeight = fontWeight, color = color)
}
 
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    CustomText(text = "Hello Compose", fontWeight = FontWeight.Bold, 
                               color = Color.Magenta)
}

When previewed, magenta-colored bold text reading “Hello Compose” will be rendered in the preview panel.

Just about any Kotlin logic code may be included in the body of a composable function. The following composable, for example, displays different text within a Column depending on the setting of a built-in Switch composable:

@Composable
fun CustomSwitch() {
 
    val checked = remember { mutableStateOf(true) }
 
    Column {
        Switch(
            checked = checked.value,
            onCheckedChange = { checked.value = it }
        )
        if (checked.value) {
            Text("Switch is On")
        } else {
            Text("Switch is Off")
        }
    }
}

In the above example, we have declared a state value named checked initialized to true and then constructed a Column containing a Switch composable. The state of the Switch is based on the value of checked and a lambda assigned as the onCheckedChanged event handler. This lambda sets the checked state to the current Switch setting. Finally, an if statement is used to decide which of two Text composables are displayed depending on the current value of the checked state. When run, the text displayed will alternate between “Switch is on” and “Switch is off”:

Figure 19-1

Similarly, we could use looping syntax to iterate through the items in a list and display them in a Column separated by instances of the Divider composable:

@Composable
fun CustomList(items: List<String>) {
    Column {
        for (item in items) {
            Text(item)
            Divider(color = Color.Black)
        }
    }
}

The following composable could be used to preview the above function:

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyApplicationTheme {
        CustomList(listOf("One", "Two", "Three", "Four", "Five", "Six"))
    }
}

Once built and refreshed, the composable will appear in the Preview panel as shown in Figure 19-2 below:

Figure 19-2

Foundation and Material composables

When developing apps with Compose we do so using a mixture of our own composable functions (for example the CustomText and CustomList composables created earlier in the chapter) combined with a set of ready to use components provided by the Compose development kit (such as the Text, Button, Column and Slider composables).

The composables bundled with Compose fall into three categories, referred to as Layout, Foundation, and Material Design components.

Layout components provide a way to define both how components are positioned on the screen, and how those components behave in relation to each other. The following are all layout composables:

  • Box
  • BoxWithConstraints
  • Column
  • ConstraintLayout
  • Row

Foundation components are a set of minimal components that provide basic user interface functionality. While these components do not, by default, impose a specific style or theme, they can be customized to provide any look and behavior you need for your app. The following lists the set of Foundation components:

  • BaseTextField
  • Canvas
  • Image
  • LazyColumn
  • LazyRow
  • Shape
  • Text

The Material Design components, on the other hand, have been designed so that they match Google’s Material theme guidelines and include the following composables:

  • AlertDialog
  • Button
  • Card
  • CircularProgressIndicator
  • DropdownMenu
  • Checkbox
  • FloatingActionButton
  • LinearProgressIndicator
  • ModalDrawer
  • RadioButton
  • Scaffold
  • Slider
  • Snackbar
  • Switch
  • TextField
  • TopAppBar
  • BottomNavigation

When choosing components, it is important to note that the Foundation and Material Design components are not mutually exclusive. You will inevitably use components from both categories in your design since the Material Design category has components for which there is no Foundation equivalent and vice versa.

Summary

In this chapter, we have looked at composable functions and explored how they are used to construct Android-based user interfaces. Composable functions are declared using the @Composable annotation and use the same syntax as standard Kotlin functions, including the passing and handling of parameters. Unlike standard Kotlin functions, composable functions do not return values. Instead, they emit user interface units to be rendered by the Compose runtime. A composable function can be either stateful or stateless depending on whether the function stores a state value. The built-in composables are categorized as either Layout, Foundation, or Material Design components. The Material Design components conform with the Material style and theme guidelines provided by Google to encourage consistent UI design.

One type of composable we have not yet introduced is the Slot API composable, a topic that will be covered later in the chapter entitled An Overview of Jetpack Compose Slot APIs.

An Overview of Jetpack Compose

Now that Android Studio has been installed and the basics of the Kotlin programing language covered, it is time to start introducing Jetpack Compose.

Jetpack Compose is an entirely new approach to developing apps for all of Google’s operating system platforms. The basic goals of Compose are to make app development easier, faster, and less prone to the types of bugs that typically appear when developing software projects. These elements have been combined with Compose-specific additions to Android Studio that allow Compose projects to be tested in near real-time using an interactive preview of the app during the development process.

Many of the advantages of Compose originate from the fact that it is both declarative and data-driven, topics which will be explained in this chapter.

The discussion in this chapter is intended as a high-level overview of Compose and does not cover the practical aspects of implementation within a project. Implementation and practical examples will be covered in detail in the remainder of the book.

Development before Compose

To understand the meaning and advantages of the Compose declarative syntax, it helps to understand how user interface layouts were designed before the introduction of Compose. Previously, Android apps were still built entirely using Android Studio together with a collection of associated frameworks that make up the Android Development Kit.

To aid in the design of the user interface layouts that make up the screens of an app, Android Studio includes a tool called the Layout Editor. The Layout Editor is a powerful tool that allows XML files to be created which contain the individual components that make up a screen of an app.

The user interface layout of a screen is designed within the Layout Editor by dragging components (such as buttons, text, text fields, and sliders) from a widget palette to the desired location on the layout canvas. Selecting a component in a scene provides access to a range of property panels where the attributes of the components can be changed.

The layout behavior of the screen (in other words how it reacts to different device screen sizes and changes to device orientation between portrait and landscape) is defined by configuring a range of constraints that dictate how each component is positioned and sized in relation to both the containing window and the other components in the layout.

Finally, any components that need to respond to user events (such as a button tap or slider motion) are connected to methods in the app source code where the event is handled.

At various points during this development process, it is necessary to compile and run the app on a simulator or device to test that everything is working as expected.

Compose declarative syntax

Compose introduces a declarative syntax that provides an entirely different way of implementing user interface layouts and behavior from the Layout Editor approach. Instead of manually designing the intricate details of the layout and appearance of components that make up a scene, Compose allows the scenes to be described using a simple and intuitive syntax. In other words, Compose allows layouts to be created by declaring how the user interface should appear without having to worry about the complexity of how the layout is built.

This essentially involves declaring the components to be included in the layout, stating the kind of layout manager in which they are to be contained (column, row, box, list, etc.), and using modifiers to set attributes such as the text on a button, the foreground color of a label, or the handler to be called in the event of a tap gesture. Having made these declarations, all the intricate and complicated details of how to position, constrain and render the layout are handled automatically by Compose.

Compose declarations are structured hierarchically, which also makes it easy to create complex views by composing together small, re-usable custom sub-views.

While a layout is being declared and tested, Android Studio provides a preview canvas that changes in realtime to reflect the appearance of the layout. Android Studio also includes an interactive preview mode which allows the app to be launched within the preview canvas and fully tested without the need to build and run on a simulator or device. Coverage of the Compose declaration syntax begins with the chapter entitled Composable Functions Overview.

Compose is data-driven

When we say that Compose is data-driven, this is not to say that it is no longer necessary to handle events generated by the user (in other words the interaction between the user and the app user interface). It is still necessary, for example, to know when the user taps a button or moves a slider and to react in some app-specific way. Being data-driven relates more to the relationship between the underlying app data and the user interface and logic of the app.

Before the introduction of Compose, an Android app would contain code responsible for checking the current values of data within the app. If data was likely to change over time, code had to be written to ensure that the user interface always reflected the latest state of the data (perhaps by writing code to frequently check for changes to the data, or by providing a refresh option for the user to request a data update). Similar challenges arise when keeping the user interface state consistent and making sure issues like toggle button settings are stored appropriately. Requirements such as these can become increasingly complex when multiple areas of an app depend on the same data sources.

Compose addresses this complexity by providing a system that is based on state. Data that is stored as state ensures that any changes to that data are automatically reflected in the user interface without the need to write any additional code to detect the change. Any user interface component that accesses a state is essentially subscribed to that state. When the state is changed anywhere in the app code, any subscriber components to that data will be destroyed and recreated to reflect the data change in a process called recomposition. This ensures that when any state on which the user interfaces is dependent changes, all components that rely on that data will automatically update to reflect the latest state. State and recomposition will be covered in the chapter entitled An Overview of Compose State and Recomposition.

Summary

Jetpack introduces a different approach to app development than that offered by the Android Studio Layout Editor. Rather than directly implement the way in which a user interface is to be rendered, Compose allows the user interface to be declared in descriptive terms and then does all the work of deciding the best way to perform the rendering when the app runs.

Compose is also data-driven in that data changes drive the behavior and appearance of the app. This is achieved through states and recomposition.

This chapter has provided a very high-level view of Jetpack Compose. The remainder of this book will explore Compose in greater depth.

An Example Jetpack Compose Project

In the previous chapter, we created a new Compose-based Android Studio project named ComposeDemo and took some time to explore both Android Studio and some of the project code that it generated to get us started. With those basic steps covered, this chapter will use the ComposeDemo project as the basis for a new app. This will involve the creation of new composable functions, introduce the concept of state, and make use of the Preview panel in interactive mode. As with the preceding chapter, key concepts explained in basic terms here will be covered in significantly greater detail in later chapters.

Getting started

Start Android Studio if it is not already running and open the ComposeDemo project created in the previous chapter. Once the project has loaded, double-click on the MainActivity.kt file (located in Project tool window under app -> java -> <package name>) to open it in the code editor. If necessary, switch the editor into Split mode so that both the editor and Preview panel are visible.

Removing the template Code

Within the MainActivity.kt file, delete some of the template code so that the file reads as follows:

package com.example.composedemo
.
.
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeDemoTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {

                }
            }
        }
    }
}

The Composable hierarchy

Before we start to write the composable functions that will make up our user interface, it helps to first visualize the relationships between these components. The ability of one composable to call other composables essentially allows us to build a hierarchy tree of components. Once completed, the composable hierarchy for our ComposeDemo main activity can be represented as shown in Figure 4-1:

Figure 4-1

All of the elements in the above diagram, except for ComponentActivity, are composable functions. Of those functions, the Surface, Column, Spacer, Text, and Slider functions are built-in composables provided by Compose. The DemoScreen, DemoText, and DemoSlider composables, on the other hand, are functions that we will create to provide both structure to the design and the custom functionality we require for our app. The ComposeDemoTheme composable declaration can be found in the ui.theme -> Theme.kt file.

Adding the DemoText composable

We are now going to add a new composable function to the activity to represent the DemoText item in the hierarchy tree. The purpose of this composable is to display a text string using a font size value which adjusts in real-time as the slider is moved. Place the cursor beneath the final closing brace (}) of the MainActivity declaration and add the following function declaration:

@Composable
fun DemoText() {
}

The @Composable annotation notifies the build system that this is a composable function. When the function is called, the plan is for it to be passed both a text string and the font size at which that text is to be displayed. This means that we need to add some parameters to the function:

@Composable
fun DemoText(message: String, fontSize: Float) {
}

The next step is to make sure the text is displayed. To achieve this, we will make a call to the built-in Text composable, passing through as parameters the message string, font size and, to make the text more prominent, a bold font weight setting:

@Composable
fun DemoText(message: String, fontSize: Float) {
    Text(
        text = message,
        fontSize = fontSize.sp,
        fontWeight = FontWeight.Bold
    )
}

Note that after making these changes, the code editor is indicating that “sp” and “FontWeight” are undefined. This is happening because these are defined and implemented in libraries that have not yet been imported into the MainActivity.kt file. One way to resolve this is to click on an undefined declaration so that it highlights as shown below, and then press Alt+Enter (Opt+Enter on macOS) on the keyboard to automatically import the missing library:

Figure 4-2

Alternatively, the missing import statements may be added manually to the list at the top of the file:

.
.
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
.
.

In the remainder of this book, all code examples will include any required library import statements.

We have now finished writing our first composable function. Notice that, except for the font weight, all the other properties are passed to the function when it is called (a function that calls another function is generally referred to as the caller). This increases the flexibility, and therefore re-usability, of the DemoText composable and is a key goal to keep in mind when writing composable functions.

Previewing the DemoText composable

At this point, the Preview panel will most likely be displaying a message which reads “No preview found”. The reason for this is that our MainActivity.kt file does not contain any composable functions prefixed with the @ Preview annotation. Add a preview composable function for DemoText to the MainActivity.kt file as follows:

@Preview
@Composable
fun DemoTextPreview() {
    DemoText(message = "Welcome to Android", fontSize = 12f)
}

After adding the preview composable, the Preview panel should have detected the change and displayed the link to build and refresh the preview rendering. Click the link and wait for the rebuild to complete, at which point the DemoText composable should appear as shown in Figure 4-3:

Figure 4-3

Minor changes made to the code in the MainActivity.kt file such as changing values will be instantly reflected in the preview without the need to build and refresh. For example, change the “Welcome to Android” text literal to “Welcome to Compose” and note that the text in the Preview panel changes as you type. Similarly, increasing the font size literal will instantly change the size of the text in the preview. This feature is referred to as Live Edit and can be enabled and disabled using the menu button indicated in Figure 4-4:

Figure 4-4

Adding the DemoSlider composable

The DemoSlider composable is a little more complicated than DemoText. It will need to be passed a variable containing the current slider position and an event handler function or lambda to call when the slider is moved by the user so that the new position can be stored and passed to the two Text composables. With these requirements in mind, add the function as follows:

.
.
import androidx.compose.foundation.layout.*
import androidx.compose.material.Slider
import androidx.compose.ui.unit.dp
.
.
@Composable
fun DemoSlider(sliderPosition: Float, onPositionChange: (Float) -> Unit ) {
    Slider(
        modifier = Modifier.padding(10.dp),
        valueRange = 20f..40f,
        value = sliderPosition,
        onValueChange = { onPositionChange(it) }
    )
}

The DemoSlider declaration contains a single Slider composable which is, in turn, passed four parameters. The first is a Modifier instance configured to add padding space around the slider. Modifier is a Kotlin class built into Compose which allows a wide range of properties to be set on a composable within a single object. Modifiers can also be created and customized in one composable before being passed to other composables where they can be further modified before being applied.

The second value passed to the Slider is a range allowed for the slider value (in this case the slider is limited to values between 20 and 40).

The next parameter sets the value of the slider to the position passed through by the caller. This ensures that each time DemoSlider is recomposed it retains the last position value.

Finally, we set the onValueChange parameter of the Slider to call the function or lambda we will be passing to the DemoSlider composable when we call it later. Each time the slider position changes, the call will be made and passed the current value which we can access via the Kotlin it keyword. We can further simplify this by assigning just the event handler parameter name (onPositionChange) and leaving the compiler to handle the passing of the current value for us:

onValueChange = onPositionChange

Adding the DemoScreen composable

The next step in our project is to add the DemoScreen composable. This will contain a variable named sliderPosition in which to store the current slider position and the implementation of the handlePositionChange event handler to be passed to the DemoSlider. This lambda will be responsible for storing the current position in the sliderPosition variable each time it is called with an updated value. Finally, DemoScreen will contain a Column composable configured to display the DemoText, Spacer, DemoSlider and the second, as yet to be added, Text composable in a vertical arrangement.

Start by adding the DemoScreen function as follows:

.
.
import androidx.compose.runtime.*
.
.
@Composable
fun DemoScreen() {
 
    var sliderPosition by remember { mutableStateOf(20f) }
 
    val handlePositionChange = { position : Float ->
        sliderPosition = position
    }
}

The sliderPosition variable declaration requires some explanation. As we will learn later, the Compose system repeatedly and rapidly recomposes user interface layouts in response to data changes. The change of slider position will, therefore, cause DemoScreen to be recomposed along with all of the composables it calls. Consider if we had declared and initialized our sliderPosition variable as follows:

var sliderPosition = 20f

Suppose the user slides the slider to position 21. The handlePositionChange event handler is called and stores the new value in the sliderPosition variable as follows:

val handlePositionChange = { position : Float ->
    sliderPosition = position
}

The Compose runtime system detects this data change and recomposes the user interface, including a call to the DemoScreen function which will, in turn, reinitialize the sliderposition variable to 20 causing the previous value of 21 to be lost. Declaring the sliderPosition variable in this way informs Compose that the current value needs to be remembered during recompositions:

var sliderPosition by remember { mutableStateOf(20f) }

The only remaining work within the DemoScreen implementation is to add a Column containing the required composable functions:

.
.
import androidx.compose.ui.Alignment
.
.
@Composable
fun DemoScreen() {
 
    var sliderPosition by remember { mutableStateOf(20f) }
 
    val handlePositionChange = { position : Float ->
        sliderPosition = position
    }
 
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
        modifier = Modifier.fillMaxSize()
    ) {
 
        DemoText(message = "Welcome to Compose", fontSize = sliderPosition)
 
        Spacer(modifier = Modifier.height(150.dp))
 
        DemoSlider(
            sliderPosition = sliderPosition,
            onPositionChange = handlePositionChange
        )
 
        Text(
            style = MaterialTheme.typography.h2,
            text = sliderPosition.toInt().toString() + "sp"
        )
    }
}

Points to note regarding these changes may be summarized as follows:

  • When DemoSlider is called, it is passed a reference to our handlePositionChange event handler as the onPositionChange parameter.
  • The Column composable accepts parameters that customize layout behavior. In this case, we have configured the column to center its children both horizontally and vertically.
  • A Modifier has been passed to the Spacer to place a 150dp vertical space between the DemoText and DemoSlider components.
  • The second Text composable is configured to use the h2 (Heading 2) style of the Material theme. The sliderPosition value is converted from a Float to an integer so that only whole numbers are displayed and then converted to a string value before being displayed to the user.

Previewing the DemoScreen composable

To confirm that the DemoScreen layout meets our expectations, we need to add a preview composable to the file. Note that the original DemoTextPreview composable may also be removed at this point:

.
.
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun Preview() {
    ComposeDemoTheme {
        DemoScreen()
    }
}

Note that we have enabled the showSystemUi property of the preview so that we will experience how the app will look when running on an Android device.

After performing a preview rebuild and refresh, the user interface should appear as originally shown in Figure 3-1.

Testing in interactive mode

At this stage, we know that the user interface layout for our activity looks how we want it to, but we don’t know if it will behave as intended. One option is to run the app on an emulator or physical device (topics which are covered in later chapters). A quicker option, however, is to switch the preview panel into interactive mode. This is achieved by clicking on the button indicated in Figure 4-5 below:

Figure 4-5

When clicked, there will be a short delay when interactive mode starts, after which it should be possible to move the slider and watch the two Text components update accordingly:

Figure 4-6

Click the stop button (marked A in Figure 4-7 below) to exit interactive mode. If it appears that the preview needs to be refreshed, simply click on the Build Refresh button (B):

Figure 4-7

Completing the project

The final step is to make sure that the DemoScreen composable is called from within the Surface function located in the onCreate() method of the MainActivity class. Locate this method and modify it as follows:

.
.
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeDemoTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    DemoScreen()
                }
            }
        }
    }
}

This will ensure that, in addition to appearing in the preview panel, our user interface will also be displayed when the app runs on a device or emulator (a topic that will be covered in later chapters).

Summary

In this chapter, we have extended our ComposeDemo project to include some additional user interface elements in the form of two Text composables, a Spacer, and a Slider. These components were arranged vertically using a Column composable. We also introduced the concept of mutable state variables and explained how they are used to ensure that the app remembers state when the Compose runtime performs recompositions. The example also demonstrated how to use event handlers to respond to user interaction (in this case the user moving a slider). Finally, we made use of the Preview panel in interactive mode to test the app without the need to compile and run it on an emulator or physical device.

Overview of a Jetpack Compose Project

Now that we have installed Android Studio, the next step is to create an Android app using Jetpack Compose. Although this project will make use of several Compose features, it is an intentionally simple example intended to provide an early demonstration of Compose in action and an initial success on which to build as you work through the remainder of the book. The project will also serve to verify that your Android Studio environment is correctly installed and configured.

This chapter will create a new project using the Android Studio Compose project template and explore both the basic structure of a Compose-based Android Studio project and some of the key areas of Android Studio. In the next chapter, we will use this project to create a simple Android app.

Both chapters will briefly explain key features of Compose as they are introduced within the project. If anything is unclear when you have completed the project, rest assured that all of the areas covered in the tutorial will be explored in greater detail in later chapters of the book.

About the project

The completed project will consist of two text components and a slider. When the slider is moved, the current value will be displayed on one of the text components, while the font size of the second text instance will adjust to match the current slider position. Once completed, the user interface for the app will appear as shown in Figure 3-1:

Figure 3-1

Creating the project

The first step in building an app is to create a new project within Android Studio. Begin, therefore, by launching Android Studio so that the “Welcome to Android Studio” screen appears as illustrated in Figure 3-2:

Figure 3-2

Once this window appears, Android Studio is ready for a new project to be created. To create the new project, click on the New Project button to display the first screen of the New Project wizard.

Creating an activity

The next step is to define the type of initial activity that is to be created for the application. The left-hand panel provides a list of platform categories from which the Phone and Tablet option must be selected. Although a range of different activity types is available when developing Android applications, only the Empty Compose Activity template provides a pre-configured project ready to work with Compose. Select this option before clicking on the Next button:

Figure 3-3

Defining the project and SDK settings

In the project configuration window (Figure 3-4), set the Name field to ComposeDemo. The application name is the name by which the application will be referenced and identified within Android Studio and is also the name that would be used if the completed application were to go on sale in the Google Play store:

Figure 3-4

The Package name is used to uniquely identify the application within the Google Play app store application ecosystem. Although this can be set to any string that uniquely identifies your app, it is traditionally based on the reversed URL of your domain name followed by the name of the application. For example, if your domain is www.mycompany.com, and the application has been named ComposeDemo, then the package name might be specified as follows:

com.mycompany.composedemo

If you do not have a domain name you can enter any other string into the Company Domain field, or you may use example.com for testing, though this will need to be changed before an application can be published:

com.example.composedemo

The Save location setting will default to a location in the folder named AndroidStudioProjects located in your home directory and may be changed by clicking on the folder icon to the right of the text field containing the current path setting.

Set the minimum SDK setting to API 26: Android 8.0 (Oreo). This is the minimum SDK that will be used in most of the projects created in this book unless a necessary feature is only available in a more recent version. The objective here is to build an app using the latest Android SDK, while also retaining compatibility with devices running older versions of Android (in this case as far back as Android 8.0). The text beneath the Minimum SDK setting will outline the percentage of Android devices currently in use on which the app will run. Click on the Help me choose link to see a full breakdown of the various Android versions still in use:

Figure 3-5

Since Compose only works with Kotlin, the Language menu is preset to Kotlin and cannot be changed. Click on the Finish button to create the project.

Previewing the example project

At this point, Android Studio should have created a minimal example application project and opened the main window.

Figure 3-6

The newly created project and references to associated files are listed in the Project tool window located on the left-hand side of the main project window. The Project tool window has several modes in which information can be displayed. By default, this panel should be in Android mode. This setting is controlled by the menu at the top of the panel as highlighted in Figure 3-7. If the panel is not currently in Android mode, use the menu to switch mode:

Figure 3-7

The code for the main activity of the project (an activity corresponds to a single user interface screen or module within an Android app) is contained within the MainActivity.kt file located under app -> java -> com.example. composedemo within the Project tool window as indicated in Figure 3-8:

Figure 3-8

Double-click on this file to load it into the main code editor panel. The editor can be used in different modes when writing code, the most useful of which when working with Compose is Split mode. The current mode can be changed using the buttons marked A in Figure 3-9. Split mode displays the code editor (B) alongside the Preview panel (C) in which the current user interface design will appear:

Figure 3-9

To get us started, Android Studio has already added some code to the MainActivity.kt file to display a Text component configured to display a message which reads “Hello Android”.

If the project has not yet been built, the Preview panel will display the message shown in Figure 3-10:

Figure 3-10

If you see this notification, click on the Build & Refresh link to rebuild the project. After the build is complete, the Preview panel should update to display the user interface defined by the code in the MainActivity.kt file:

Figure 3-11

Reviewing the main activity

Android applications are created by bringing together one or more elements known as Activities. An activity is a single, standalone module of application functionality that either correlates directly to a single user interface screen and its corresponding functionality, or acts as a container for a collection of related screens. An appointments application might, for example, contain an activity screen that displays appointments set up for the current day. The application might also utilize a second activity consisting of multiple screens where new appointments may be entered by the user and existing appointments edited.

When we created the ComposeDemo project, Android Studio created a single initial activity for our app, named it MainActivity, and generated some code for it in the MainActivity.kt file. This activity contains the first screen that will be displayed when the app is run on a device. Before we modify the code for our requirements in the next chapter, it is worth taking some time to review the code currently contained within the MainActivity.kt file.

The file begins with the following line (keep in mind that this may be different if you used your own domain name instead of com.example):

package com.example.composedemo

This tells the build system that the classes and functions declared in this file belong to the com.example. composedemo package which we configured when we created the project.

Next are a series of import directives. The Android SDK is comprised of a vast collection of libraries that provide the foundation for building Android apps. If all of these libraries were included within an app the resulting app bundle would be too large to run efficiently on a mobile device. To avoid this problem an app only imports the libraries that it needs to be able to run:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
.
.
.

Initially, the list of import directives will most likely be “folded” to save space. To unfold the list, click on the small “+” button indicated by the arrow in Figure 3-12 below:

Figure 3-12

The MainActivity class is then declared as a subclass of the Android ComponentActivity class:

class MainActivity : ComponentActivity() {
.
.
}

The MainActivity class implements a single method in the form of onCreate(). This is the first method that is called when an activity is launched by the Android runtime system and is an artifact of the way apps used to be developed before the introduction of Compose. The onCreate() method is used here to provide a bridge between the containing activity and the Compose-based user interfaces that are to appear within it:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        ComposeDemoTheme {
.
.
        }
    }
}

The method declares that the content of the activity’s user interface will be provided by a composable function named ComposeDemoTheme. This composable function is declared in the Theme.kt file located under the app -> <package name> -> ui.theme folder in the Project tool window. This, along with the other files in the ui.theme folder defines the colors, fonts, and shapes to be used by the activity and provides a central location from which to customize the overall theme of the app’s user interface.

The call to the ComposeDemoTheme composable function is configured to contain a Surface composable. Surface is a built-in Compose component designed to provide a background for other composables:

ComposeDemoTheme {
    // A surface container using the 'background' color from the theme
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = MaterialTheme.colors.background
.
.
}

In this case, the Surface component is configured to fill the entire screen and with the background set to the standard background color defined by the Android Material Design theme. Material Design is a set of design guidelines developed by Google to provide a consistent look and feel across all Android apps. It includes a theme (including fonts and colors), a set of user interface components (such as button, text, and a range of text fields), icons, and generally defines how an Android app should look, behave and respond to user interactions.

Finally, the Surface is configured to contain a composable function named Greeting which is passed a string value that reads “Android”:

ComposeDemoTheme {
    // A surface container using the 'background' color from the theme
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = MaterialTheme.colors.background
    ) {
        Greeting("Android")
    }
}

Outside of the scope of the MainActivity class, we encounter our first composable function declaration within the activity. The function is named Greeting and is, unsurprisingly, marked as being composable by the @ Composable annotation:

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

The function accepts a String parameter (labeled name) and calls the built-in Text composable, passing through a string value containing the word “Hello” concatenated with the name parameter. As will soon become evident as you work through the book, composable functions are the fundamental building blocks for developing Android apps using Compose.

The second composable function declared in the MainActivity.kt file reads as follows:

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    ComposeDemoTheme {
        Greeting("Android")
    }
}

Earlier in the chapter, we looked at how the Preview panel allows us to see how the user interface will appear without having to compile and run the app. At first glance, it would be easy to assume that the preview rendering is generated by the code in the onCreate() method. In fact, that method only gets called when the app runs on a device or emulator. Previews are generated by preview composable functions. The @Preview annotation associated with the function tells Android Studio that this is a preview function and that the content emitted by the function is to be displayed in the Preview panel. As we will see later in the book, a single activity can contain multiple preview composable functions configured to preview specific sections of a user interface using different data values.

In addition, each preview may be configured by passing parameters to the @Preview annotation. For example, to view the preview with the rest of the standard Android screen decorations, modify the preview annotation so that it reads as follows:

@Preview(showSystemUi = true)

Once the preview has been updated, it should now be rendered as shown in Figure 3-13:

Figure 3-13

Preview updates

One final point worth noting is that the Preview panel is live and will automatically reflect minor changes made to the composable functions that make up a preview. To see this in action, edit the call to the Greeting function in the DefaultPreview() preview composable function to change the name from “Android” to “Compose”. Note that as you make the change in the code editor, it is reflected in the preview.

More significant changes will require a build and refresh before being reflected in the preview. When this is required, Android Studio will display the following notice at the top of the Preview panel:

Figure 3-14

Simply click on the Build & Refresh link to update the preview for the latest changes.

The Preview panel also includes an interactive mode that allows you to trigger events on the user interface components (for example clicking buttons, moving sliders, scrolling through lists, etc.). Since ComposeDemo contains only an inanimate Text component at this stage, it makes more sense to introduce interactive mode in the next chapter.

Summary

In this chapter, we have created a new project using Android Studio’s Empty Compose Activity template and explored some of the code automatically generated for the project. We have also introduced several features of Android Studio designed to make app development with Compose easier. The most useful features, and the places where you will spend most of your time while developing Android apps, are the code editor and Preview panel.

While the default code in the MainActivity.kt file provides an interesting example of a basic user interface, it bears no resemblance to the app we want to create. In the next chapter, we will modify and extend the app by removing some of the template code and writing our own composable functions.