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:

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

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.

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

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:

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

@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:

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

@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

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

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:

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

<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:

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

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:

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

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

 

You are reading a sample chapter from Jetpack Compose 1.2 Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

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.