Chapters

Hide chapters

Jetpack Compose by Tutorials

First Edition · Android 11 · Kotlin 1.4 · Android Studio Canary - Arctic Fox Release

5. Combining Composables
Written by Denis Buketa

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Great job on completing the first section of this book! Now that you know the basic pillars of Compose, you have everything you need to tackle the challenges of the second section.

The goal of the second section is to show you Jetpack Compose in action. Over the course of this section, you’ll build Jet Notes, a simple but functional app for managing notes.

Each chapter in this section will explain certain concepts that you’ll apply to gradually build the different parts of the app. Note that you might build some components in one chapter, but integrate them in the next one. Likewise, you might start working on a specific component but finish it in a different chapter. But don’t worry, when you finish the whole section, you’ll have your own app written entirely with Jetpack Compose and working as expected! :]

By now, you’ve heard a lot about the basic composables that Jetpack Compose provides for you. In this chapter:

  • You’ll learn how to think about UI design when building it with Jetpack Compose.
  • You’ll see how you can combine basic composables to create complex UI.
  • You’ll create two components with different complexity for Jet Notes.

Let’s first explore the features you’ll build for your app.

Application features

Before you start writing code, have a look at the app concept and its features:

Application Overview
Application Overview

Don’t worry about the details on each screen. You’ll have a chance to see it more closely when you start implementing each screen. As you see, Jet Notes contains four main components: a Notes screen, a Save Note screen, a Trash screen and an app drawer.

The Notes screen displays the list of created notes. From here, the user can open an existing note, create a new one or open the app drawer.

The Save Note screen has two modes: an edit mode and a create a new note mode. When the user clicks on a note in the Notes screen, the Save Note screen will open in edit mode. The user can then edit the note or simply move it to the Trash screen by clicking a trash icon on the app bar.

To create a new note, the user taps on the Floating Action Button (FAB) available in the Notes screen. That opens the Save Note screen in the mode for creating a new note.

There are two types of notes: regular notes and checkable notes. Checkable notes are notes that the user can mark — or check — as done. The user can make any note checkable by using a switch component in the Save Note screen. In the Notes screen, checkable notes have a checkbox to mark the note as done.

Tapping the navigation icon on the app bar or swiping from the left border of the screen opens the app drawer. The app drawer switches between the Notes and the Trash screens. Using the drawer, a user can also change the app’s theme from light to dark.

In the Trash screen, the user can switch between regular and checkable notes using two tabs. The user can select notes and restore them or delete them permanently.

By the end of this second section, your app will have all of the features mentioned above.

Now that you’ve familiarized yourself with the app and its features, it’s time to start coding! :]

Project overview

To follow along with the code examples, open this chapter’s starter project using Android Studio and select Open an existing project. Navigate to 05-creating-custom-composables/projects and select the starter folder as the project root.

Project Structure
Qluxizn Tcwuskawo

Empty Starter Project — App State
Utqrf Fzepnej Gzulets — Evb Gqowa

Thinking in Compose

Before you start coding, look at the Notes screen design once more and try to think in Compose. In other words, break the design into modular components that you can combine to form the whole screen.

Notes Screen — Components
Weguf Wvjaiv — Yadtofonpw

Bottom-up approach

When building your apps with Jetpack Compose, it’s smart to start with smaller composables and build your way up through the design. You call this way of working a bottom-up approach.

Note Component
Pilu Hefyenupy

Creating the Note composable

Use Android Studio to create a new package called ui.components. Then, in that package, create a new Kotlin file named Note.kt. Finally, add the following code to Note.kt:

import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun Note() {

}

@Preview
@Composable
private fun NotePreview() {
  Note()
}
Android Studio - Preview
Epqbaag Mkanuu - Pwoyuum

Emitting the note’s content

Now that you’ve built the Note composable, your next step is to add the code that will emit the note’s content. Add the following code to Note()’s body:

Box(
  modifier = Modifier
    .size(40.dp)
    .background(rwGreen)
)
Text(text = "Title", maxLines = 1)
Text(text = "Content", maxLines = 1)
Checkbox(
  checked = false,
  onCheckedChange = { },
  modifier = Modifier.padding(start = 8.dp)
)
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.raywenderlich.android.jetnotes.theme.rwGreen
Note Composable — Preview
Paso Nagfahazwi — Whehaaq

Note Composable - Tree Hierarchy
Kero Wowrukebke - Xnoi Joaqinbzd

Row(modifier = Modifier.fillMaxWidth()) {
  Box(
    modifier = Modifier
      .size(40.dp)
      .background(rwGreen)
  )
  Column(modifier = Modifier.weight(1f)) {
    Text(text = "Title", maxLines = 1)
    Text(text = "Content", maxLines = 1)
  }
  Checkbox(
    checked = false,
    onCheckedChange = { },
    modifier = Modifier.padding(start = 8.dp)
  )
}
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth

Building the app drawer composable

The next thing you’ll do is a little more complex. You’ll create an AppDrawer() to switch screens and to change the app’s theme.

App Drawer — Components
Agq Mnoped — Quymafeccy

Adding a header to the drawer

Once again, you’ll take the bottom-up approach to building the AppDrawer(). You’ll implement smaller components first, then combine them. In ui.components, create a new file named AppDrawer.kt. Then, add the following code to it:

@Composable
private fun AppDrawerHeader() {
  Row(modifier = Modifier.fillMaxWidth()) {
    Image(
      imageVector = Icons.Filled.Menu,
      contentDescription = "Drawer Header Icon",
      colorFilter = ColorFilter
        .tint(MaterialTheme.colors.onSurface),
      modifier = Modifier.padding(16.dp)
    )
    Text(
      text = "JetNotes",
      modifier = Modifier
        .align(alignment = Alignment.CenterVertically)
    )
  }
}

@Preview
@Composable
fun AppDrawerHeaderPreview() {
  JetNotesTheme {
    AppDrawerHeader()
  }
}
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.raywenderlich.android.jetnotes.theme.JetNotesTheme
@Preview
@Composable
fun AppDrawerHeaderPreview() {
  JetNotesTheme {
    AppDrawerHeader()
  }
}
AppDrawerHeader Composable — Preview
UkfPbayuyBiutut Damtohorju — Hnahuox

Creating the navigation button composable

Next, you’ll create a composable for modeling the navigation buttons that switch between screens. To do so, add the following code to AppDrawer.kt:

@Composable
private fun ScreenNavigationButton(
  icon: ImageVector,
  label: String,
  isSelected: Boolean,
  onClick: () -> Unit
) {
  val colors = MaterialTheme.colors

  // Define alphas for the image for two different states 
  // of the button: selected/unselected
  val imageAlpha = if (isSelected) {
    1f
  } else {
    0.6f
  }

  // Define color for the text for two different states 
  // of the button: selected/unselected
  val textColor = if (isSelected) {
    colors.primary
  } else {
    colors.onSurface.copy(alpha = 0.6f)
  }

  // Define color for the background for two different states 
  // of the button: selected/unselected
  val backgroundColor = if (isSelected) {
    colors.primary.copy(alpha = 0.12f)
  } else {
    colors.surface
  }
}
import androidx.compose.ui.graphics.vector.ImageVector
Surface( // 1
  modifier = Modifier
    .fillMaxWidth()
    .padding(start = 8.dp, end = 8.dp, top = 8.dp),
  color = backgroundColor,
  shape = MaterialTheme.shapes.small
) {
  Row( // 2
    horizontalArrangement = Arrangement.Start,
    verticalAlignment = Alignment.CenterVertically,
    modifier = Modifier
      .clickable(onClick = onClick)
      .fillMaxWidth()
      .padding(4.dp)
  ) {
    Image(
      imageVector = icon,
      contentDescription = "Screen Navigation Button",
      colorFilter = ColorFilter.tint(textColor),
      alpha = imageAlpha
    )
    Spacer(Modifier.width(16.dp)) // 3
    Text(
      text = label,
      style = MaterialTheme.typography.body2,
      color = textColor,
      modifier = Modifier.fillMaxWidth()
    )
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material.Surface
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
@Preview
@Composable
fun ScreenNavigationButtonPreview() {
  JetNotesTheme {
    ScreenNavigationButton(
      icon = Icons.Filled.Home,
      label = "Notes",
      isSelected = true,
      onClick = { }
    )
  }
}
import androidx.compose.material.icons.filled.Home
ScreenNavigationButton Composable — Preview
LryearFuyozefuiqRudbur Garnewocya — Rrixuaw

Adding a theme switcher

The theme switcher is a toggle button that lets the user change the app’s theme from light to dark.

@Composable
private fun LightDarkThemeItem() {
  Row(
    Modifier
      .padding(8.dp)
  ) {
    Text(
      text = "Turn on dark theme",
      style = MaterialTheme.typography.body2,
      color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
      modifier = Modifier
        .weight(1f)
        .padding(start = 8.dp, top = 8.dp, end = 8.dp, bottom = 8.dp)
        .align(alignment = Alignment.CenterVertically)
    )
    Switch(
      checked = JetNotesThemeSettings.isDarkThemeEnabled,
      onCheckedChange = { JetNotesThemeSettings.isDarkThemeEnabled = it },
      modifier = Modifier
        .padding(start = 8.dp, end = 8.dp)
        .align(alignment = Alignment.CenterVertically)
    )
  }
}

@Preview
@Composable
fun LightDarkThemeItemPreview() {
  JetNotesTheme {
    LightDarkThemeItem()
  }
}
import androidx.compose.material.Switch
import com.raywenderlich.android.jetnotes.theme.JetNotesThemeSettings
LightDarkThemeItem Composable — Preview
HeftyWogfRwagiUtun Jokkerofhi — Mmanoiz

Wrapping up the app drawer

In the previous sections, you created the different building blocks that you need to build the drawer. Now, you need to put them all together. To do so, add the following code to AppDrawer.kt:

@Composable
fun AppDrawer(
  currentScreen: Screen,
  closeDrawerAction: () -> Unit
) {
  Column(modifier = Modifier.fillMaxSize()) {
    AppDrawerHeader()

    Divider(color = MaterialTheme.colors.onSurface.copy(alpha = .2f))

    ScreenNavigationButton(
      icon = Icons.Filled.Home,
      label = "Notes",
      isSelected = currentScreen == Screen.Notes,
      onClick = {
        JetNotesRouter.navigateTo(Screen.Notes)
        closeDrawerAction()
      }
    )
    ScreenNavigationButton(
      icon = Icons.Filled.Delete,
      label = "Trash",
      isSelected = currentScreen == Screen.Trash,
      onClick = {
        JetNotesRouter.navigateTo(Screen.Trash)
        closeDrawerAction()
      }
    )
    LightDarkThemeItem()
  }
}
import androidx.compose.material.icons.filled.Delete
import com.raywenderlich.android.jetnotes.routing.JetNotesRouter
import com.raywenderlich.android.jetnotes.routing.Screen
import androidx.compose.material.Divider
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@Preview
@Composable
fun AppDrawerPreview() {
  JetNotesTheme {
    AppDrawer(Screen.Notes, {})
  }
}
LightDarkThemeItem Composable — Preview
VebmbTebbBbajaIqur Katdecirwi — Pjequap

AppDrawer Composable — Compose Tree
EjdPgedig Jiqnofurqe — Powxage Xpea

Putting all the pieces together

After all this work, it would be a shame not to see the different composables you built working together in your app. So your final step will be to put the puzzle pieces together.

JetNotesTheme {
  val coroutineScope = rememberCoroutineScope()
  val scaffoldState: ScaffoldState = rememberScaffoldState()

  Scaffold(
    scaffoldState = scaffoldState,
    drawerContent = {
      AppDrawer(
        currentScreen = Screen.Notes,
        closeDrawerAction = { 
          coroutineScope.launch {
            scaffoldState.drawerState.close()
          }
        }
      )
    },
    content = {
      Note()
    }
  )
}
import androidx.compose.material.Scaffold
import androidx.compose.material.ScaffoldState
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.rememberCoroutineScope
import com.raywenderlich.android.jetnotes.routing.Screen
import com.raywenderlich.android.jetnotes.theme.JetNotesTheme
import com.raywenderlich.android.jetnotes.ui.components.AppDrawer
import com.raywenderlich.android.jetnotes.ui.components.Note
import kotlinx.coroutines.launch
Note Composable and App Drawer Composable
Kune Nogbokeryo egl Avs Wwuqaf Yutnihellu

Key points

  • Before implementing a UI design, break it down into modular components that work together to make the whole screen.
  • When implementing a specific UI design, use a bottom-up approach. Start with smaller composables and build your way up through the design. This will let you decouple and reuse code from the very start.
  • Use the Preview feature in Android Studio to visualize and inspect your composables.
  • Every complex composable is built from basic composables that work together. It’s a small puzzle of simple elements.
  • When you add composable functions, you’re describing the hierarchy of the elements that will render on the screen.
  • Calling composable functions produces a tree, where each node is a composable function.

Where to go from here?

Congratulations on finishing the chapter! I hope it was a nice ride for you! If you enjoyed building your custom composables, get ready because things are going to get more interesting in the following chapters. :]

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now