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.

Jetpack Compose Essentials – Introduction

Start Here

The goal of this book is to teach you how to build Android applications using Jetpack Compose, Android Studio, and the Kotlin programming language.

Beginning with the basics, this book explains how to set up an Android Studio development environment.

The book also includes in-depth chapters introducing the Kotlin programming language including data types, operators, control flow, functions, lambdas, and object-oriented programming.

An introduction to the key concepts of Jetpack Compose and Android project architecture is followed by a guided tour of Android Studio in Compose development mode. The book also covers the creation of custom Composables and explains how these functions are combined to create user interface layouts including the use of row, column, box, and list components.

Other topics covered include data handling using state properties, key user interface design concepts such as modifiers, navigation bars, and user interface navigation. Additional chapters explore building your own reusable custom layout components.

The book also includes chapters covering graphics drawing, user interface animation, transitions, and gesture handling.

Chapters are also included covering view models, SQLite databases, Room database access, the Database Inspector, live data, and custom theme creation.

Finally, the book explains how to package up a completed app and upload it to the Google Play Store for publication.

Along the way, the topics covered in the book are put into practice through detailed tutorials, the source code for which is also available for download.

Assuming you already have some rudimentary programming experience, are ready to download Android Studio and the Android SDK, and have access to a Windows, Mac, or Linux system, you are ready to get started.

For Kotlin programmers

This book has been designed to address the needs of both existing Kotlin programmers and those who are new to both Kotlin and Jetpack Compose app development. If you are familiar with the Kotlin programming language, you can probably skip the Kotlin specific chapters.

For new Kotlin programmers

If you are new to programming in Kotlin then the entire book is appropriate for you. Just start at the beginning and keep going.

Downloading the code samples

The source code and Android Studio project files for the examples contained in this book are available for download at:

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

The steps to load a project from the code samples into Android Studio are as follows:

  1. From the Welcome to Android Studio dialog, click on the Open button option.
  2. In the project selection dialog, navigate to and select the folder containing the project to be imported and click on OK.

Feedback

We want you to be satisfied with your purchase of this book. If you find any errors in the book or have any comments, questions or concerns please contact us at [email protected].

Errata

While we make every effort to ensure the accuracy of the content of this book, inevitably, a book covering a subject area of this size and complexity may include some errors and oversights. Any known issues with the book will be outlined, together with solutions at the following URL:

https://www.ebookfrenzy.com/errata/compose.html

If you find an error not listed in the errata, please let us know by emailing our technical support team at [email protected] ebookfrenzy.com.