Chapters

Hide chapters

Jetpack Compose by Tutorials

First Edition · Android 11 · Kotlin 1.4 · Android Studio Canary

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

8. Applying Material Design to Compose
Written by Denis Buketa

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Well done! You’ve arrived at the last chapter in this section. In your journey so far, you’ve learned about basic composables in Compose and how to combine, style and use them in a real app where you also had to manage state.

In this chapter, you’ll:

  • Learn how to use Material Design composables, which Jetpack Compose provides for you.
  • Go over state management in more depth.
  • Complete the Save Note screen.
  • Learn about Material theming.
  • Change JetNotes to support a dark theme.

When you finish this chapter, JetNotes will be a completely functional app!

Opening the Notes screen

Before you can start working on the Save Note screen, you need a way to open it. By looking at the design, you can see that you’ve planned two ways to do that:

  1. By clicking a floating action button (FAB), which will open the Save Note screen in Create mode, where the user create a new note.
  2. By clicking any note on the Notes screen, which opens it in Edit mode, where the user can edit that specific note.

You’ll start with the first case. However, before adding a floating action button to the Notes screen, you need to add some layout structure to it.

Notes Screen
Notes Screen

Take a moment to look at the different parts of the screen. You have the:

  • Top bar
  • Body content
  • Floating action button
  • App drawer

This is a common layout structure for Android apps. Most apps today follow a similar design. To make it easier to implement a layout structure like this, Jetpack Compose provides the Scaffold.

Before going into any details, you’ll add a Scaffold to the Notes screen.

Adding Scaffold

To follow along with the code examples, open this chapter’s starter project in Android Studio and select Open an existing project.

@Composable
fun NotesScreen(viewModel: MainViewModel) {

  // Observing notes state from MainViewModel
  ...

  Scaffold(
    topBar = {
      TopAppBar(
        title = "JetNotes",
        icon = Icons.Filled.List,
        onIconClick = {}
      )
    },
    bodyContent = {
      if (notes.isNotEmpty()) {
        NotesList(
          notes = notes,
          onNoteCheckedChange = { 
          	viewModel.onNoteCheckedChange(it) 
          },
          onNoteClick = { viewModel.onNoteClick(it) }
        )
      }
    }
  )
}
import androidx.compose.material.Scaffold
Notes Screen
Puxil Hsfaib

@Composable 
fun Scaffold(
  modifier: Modifier = Modifier,
  scaffoldState: ScaffoldState = rememberScaffoldState(),
  topBar: @Composable () -> Unit = emptyContent(), // Top Bar
  bottomBar: @Composable () -> Unit = emptyContent(), // Bottom Bar
  snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
  floatingActionButton: @Composable () -> Unit = emptyContent(), // FAB
  floatingActionButtonPosition: FabPosition = FabPosition.End,
  isFloatingActionButtonDocked: Boolean = false,
  drawerContent: @Composable (ColumnScope.() -> Unit)? = null, // Navigation Drawer
  drawerGesturesEnabled: Boolean = true,
  drawerShape: Shape = MaterialTheme.shapes.large,
  drawerElevation: Dp = DrawerConstants.DefaultElevation,
  drawerBackgroundColor: Color = MaterialTheme.colors.surface,
  drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
  drawerScrimColor: Color = DrawerConstants.defaultScrimColor,
  backgroundColor: Color = MaterialTheme.colors.background,
  contentColor: Color = contentColorFor(backgroundColor),
  bodyContent: @Composable (PaddingValues) -> Unit // Screen Content
)

Resurrecting the app drawer

In the previous chapter, you temporarily removed the app drawer from the Notes screen. Now, it’s time to put it back, slightly improved.

@Composable
fun NotesScreen(viewModel: MainViewModel) {

  // Observing notes state from MainViewModel
  ...

  // here - Drawer state
  val scaffoldState: ScaffoldState = rememberScaffoldState()

  Scaffold(
    topBar = {
      TopAppBar(
        title = "JetNotes",
        icon = Icons.Filled.List,
        onIconClick = { 
          // here - Drawer open
          scaffoldState.drawerState.open() 
        }
      )
    },
    scaffoldState = scaffoldState, // here - Scaffold state
    drawerContent = { // here - Drawer UI
      AppDrawer(
        currentScreen = Screen.Notes,
        closeDrawerAction = { 
          // here - Drawer close
          scaffoldState.drawerState.close()
        }
      )
    },
    ...
  )
}
import androidx.compose.material.ScaffoldState
import androidx.compose.material.rememberScaffoldState
import com.raywenderlich.android.jetnotes.routing.Screen
import com.raywenderlich.android.jetnotes.ui.components.AppDrawer
Notes screen and app drawer
Koyij fzjoar exw etb wlaful

Memory in composable functions

Scaffold() can manage two composables that have state: app drawer and snackbar. Their states, DrawerState and SnackbarHostState, are encapsulated in one object called ScaffoldState.

Using remember

Here’s how remember() looks in code:

@Composable
fun <T> remember(calculation: () -> T): T
@Composable
fun rememberScaffoldState(
  drawerState: DrawerState = rememberDrawerState(
    DrawerValue.Closed
  ), 
  snackbarHostState: SnackbarHostState = remember { 
    SnackbarHostState() 
  }
): ScaffoldState
@Composable
fun rememberDrawerState(
    initialValue: DrawerValue,
    confirmStateChange: (DrawerValue) -> Boolean = { true }
): DrawerState {
  val clock = AmbientAnimationClock.current.asDisposableClock()
  return rememberSavedInstanceState(
    clock,
    saver = DrawerState.Saver(clock, confirmStateChange)
  ) {
    DrawerState(initialValue, clock, confirmStateChange)
  }
}

Remember’s effect on the composition tree

Here’s how the composition tree looks for NotesScreen().

Notes Screen - Composition Tree
Giluk Smteek - Zoxguheluug Wduu

Adding the FAB

A floating action button represents the primary action of a screen. In the Notes screen, the primary action is the action to create a new note.

@Composable
fun NotesScreen(viewModel: MainViewModel) {

  // Observing notes state from MainViewModel
  ...

  val scaffoldState: ScaffoldState = rememberScaffoldState()

  Scaffold(
    ...,
    floatingActionButtonPosition = FabPosition.End,
    floatingActionButton = {
      FloatingActionButton(
          onClick = { viewModel.onCreateNewNoteClick() },
          contentColor = MaterialTheme.colors.background,
          content = { Icon(Icons.Filled.Add) }
      )
    },
    ...
  )
}
import androidx.compose.material.*
import androidx.compose.material.icons.filled.Add
Notes screen with floating action button
Radab tmweih rirf zbaoxegp ukwuaz kavxor

Adding an entry point

In the previous section, you added the FAB that allows you to open the Save Note screen in the Create mode.

@Composable
fun SaveNoteScreen(viewModel: MainViewModel) {

}
import com.raywenderlich.android.jetnotes.viewmodel.MainViewModel

Using JetNotesRouter to change screens

In the previous chapter, you added the code that opens the Notes screen whenever you start MainActivity. It’s time to add logic to change screens with JetNotesRouter.

@Composable
private fun MainActivityScreen(viewModel: MainViewModel) {
  Surface {
    when (JetNotesRouter.currentScreen) {
      is Screen.Notes -> NotesScreen(viewModel)
      is Screen.SaveNote -> SaveNoteScreen(viewModel)
      is Screen.Trash -> TrashScreen(viewModel)
    }
  }
}
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import com.raywenderlich.android.jetnotes.routing.JetNotesRouter
import com.raywenderlich.android.jetnotes.routing.Screen
import com.raywenderlich.android.jetnotes.ui.screens.SaveNoteScreen
import com.raywenderlich.android.jetnotes.ui.screens.TrashScreen

Connecting your composable to MainActivity

Next, you’ll connect this composable to MainActivity. Update setContent() in the MainActivity:

class MainActivity : AppCompatActivity() {

  ...

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

    setContent {
      JetNotesTheme {
        MainActivityScreen(viewModel = viewModel) // here
      }
    }
  }
}

Calling JetNotesRouter

Open MainViewModel.kt and update onCreateNewNoteClick():

class MainViewModel(private val repository: Repository) : ViewModel() {

  ...

  fun onCreateNewNoteClick() {
    JetNotesRouter.navigateTo(Screen.SaveNote)
  }

  ...
}
import com.raywenderlich.android.jetnotes.routing.JetNotesRouter
import com.raywenderlich.android.jetnotes.routing.Screen
Opening Save Notes screen
Oraserb Laku Diral thnian

Adding the top bar

Until now, you’ve focused on adding code to open the Save Note screen. But now that you can open it, the Save Note screen is empty. In this section, you’ll add composables to it. :]

@Composable
fun SaveNoteScreen(viewModel: MainViewModel) {
  Scaffold(
    topBar = {},
    bodyContent = {}
  )
}
import androidx.compose.material.Scaffold
Save Note Screen: Top bar
Keyu Diji Zmjoog: Soc qef

Adding SaveNoteTopAppBar

In the Save Note screen, the top bar needs to support two different modes:

@Composable
private fun SaveNoteTopAppBar(
  isEditingMode: Boolean,
  onBackClick: () -> Unit,
  onSaveNoteClick: () -> Unit,
  onOpenColorPickerClick: () -> Unit,
  onDeleteNoteClick: () -> Unit
) {

}

Displaying the top bar

Now that you’ve prepared the root composable for the top bar, you’ll add the composable that emits the top bar in the UI.

TopAppBar(
  title = {
    Text(
      text = "Save Note",
      color = MaterialTheme.colors.onPrimary
    )
  }
)
navigationIcon = {
  IconButton(onClick = onBackClick) {
    Icon(imageVector = Icons.Filled.ArrowBack)
  }
}
actions = {
  // Save note action icon
  IconButton(onClick = onSaveNoteClick) {
    Icon(
      imageVector = Icons.Default.Check,
      tint = MaterialTheme.colors.onPrimary
    )
  }

  // Open color picker action icon
  IconButton(onClick = onOpenColorPickerClick) {
    Icon(
      imageVector = vectorResource(
        id = R.drawable.ic_baseline_color_lens_24
      ),
      tint = MaterialTheme.colors.onPrimary
    )
  }
}
// Delete action icon (show only in editing mode)
if (isEditingMode) {
  IconButton(onClick = onDeleteNoteClick) {
    Icon(
      imageVector = Icons.Default.Delete,
      tint = MaterialTheme.colors.onPrimary
    )
  }
}
import androidx.compose.material.*
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete
import androidx.compose.ui.res.vectorResource
import com.raywenderlich.android.jetnotes.R
@Preview
@Composable
fun SaveNoteTopAppBarPreview() {
  SaveNoteTopAppBar(
    isEditingMode = true,
    onBackClick = {},
    onSaveNoteClick = {},
    onOpenColorPickerClick = {},
    onDeleteNoteClick = {}
  )
}
SaveNoteTopAppBar Composable (Editing mode) — Preview
PeloNajeCibUtnVaq Kibzanetno (Opafupv novo) — Nzogein

SaveNoteTopAppBar Composable (New Note mode) — Preview
RujoKaviRadImdWow Gofpafuyro (Lep Bugo jepi) — Mzavauh

Displaying the SaveNoteTopAppBar composable

Now that you’ve created the SaveNoteTopAppBar(), you can display it in the Save Note screen. But before you do that, you need a way of knowing if the user opened the Save Note screen for a new note or an existing note.

private var _noteEntry = MutableLiveData(NoteModel())
val noteEntry: LiveData<NoteModel> = _noteEntry
@Composable
fun SaveNoteScreen(viewModel: MainViewModel) {

  val noteEntry: NoteModel by viewModel.noteEntry
    .observeAsState(NoteModel())

  Scaffold(
    topBar = {
      val isEditingMode: Boolean = noteEntry.id != NEW_NOTE_ID
      SaveNoteTopAppBar(
        isEditingMode = isEditingMode,
        onBackClick = { 
          JetNotesRouter.navigateTo(Screen.Notes) 
        },
        onSaveNoteClick = { },
        onOpenColorPickerClick = { },
        onDeleteNoteClick = { }
      )
    },
    bodyContent = {}
  )
}
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import com.raywenderlich.android.jetnotes.domain.model.NEW_NOTE_ID
import com.raywenderlich.android.jetnotes.domain.model.NoteModel
import com.raywenderlich.android.jetnotes.routing.JetNotesRouter
import com.raywenderlich.android.jetnotes.routing.Screen
Adding the top bar to the Save Note screen
Eqxunj lbu gik los ka shu Qura Peda gbwaev

Opening the Save Note screen in Editing mode

In the previous section, you implemented a way to open the Save Note screen in Create mode. Now, you’ll add the logic that allows the user to edit an existing note.

fun onCreateNewNoteClick() {
  _noteEntry.value = NoteModel()
  JetNotesRouter.navigateTo(Screen.SaveNote)
}

fun onNoteClick(note: NoteModel) {
  _noteEntry.value = note
  JetNotesRouter.navigateTo(Screen.SaveNote)
}
Save Note screen in edit mode
Dulo Defa vsjeop ih uhav sofa

Creating a content composable

You need to be able to edit notes in the Save Note screen, so your next step is to create a content composable to let you do that.

Displaying the selected color

To do this, go to SaveNoteScreen.kt and add the following composable below SaveNoteTopAppBar():

@Composable
private fun PickedColor(color: ColorModel) {
  Row(
    Modifier
      .padding(8.dp)
      .padding(top = 16.dp)
  ) {
    Text(
      text = "Picked color",
      modifier = Modifier
        .weight(1f)
        .align(Alignment.CenterVertically)
    )
    NoteColor(
      color = Color.fromHex(color.hex),
      size = 40.dp,
      border = 1.dp,
      modifier = Modifier.padding(4.dp)
    )
  }
}
@Preview
@Composable
fun PickedColorPreview() {
  PickedColor(ColorModel.DEFAULT)
}
PickedColorComponent Composable — Preview
ZelpalHaxamNupcoyonm Gesqayiwru — Dqetoug

Letting users check off a note

In some cases, your users might want to check off a note — when they’ve completed a task, for example. By default, there’s no option to indicate that a note has been completed. Users need to mark notes as checkable if they want that feature. Your next step is to give them that possibility.

@Composable
private fun NoteCheckOption(
  isChecked: Boolean,
  onCheckedChange: (Boolean) -> Unit
) {
  Row(
    Modifier
      .padding(8.dp)
      .padding(top = 16.dp)
  ) {
    Text(
      text = "Can note be checked off?",
      modifier = Modifier.weight(1f)
    )
    Switch(
      checked = isChecked,
      onCheckedChange = onCheckedChange,
      modifier = Modifier.padding(start = 8.dp)
    )
  }
}
import androidx.compose.material.Switch
@Preview
@Composable
fun NoteCheckOptionPreview() {
  NoteCheckOption(false) {}
}
CanBeCheckedOffComponent Composable — Preview
TohQeYgekluhIhlDabzazuky Mutdakodru — Kgewoup

Adding a title and content

So far, you’ve added composables to represent the note’s color and whether the user can check the note off when they complete a task. But you still have to add composables for the most important parts of the note: its title and content.

@Composable
private fun ContentTextField(
  modifier: Modifier = Modifier,
  label: String,
  text: String,
  onTextChange: (String) -> Unit
) {
  TextField(
    value = text,
    onValueChange = onTextChange,
    label = { Text(label) },
    modifier = modifier
      .fillMaxWidth()
      .padding(horizontal = 8.dp),
    backgroundColor = MaterialTheme.colors.surface
  )
}
@Preview
@Composable
fun ContentTextFieldPreview() {
  ContentTextField(
    label = "Title", 
    text = "", 
    onTextChange = {}
  )
}
ContentTextField Composable — Preview
MuqxojxHexlHuumb Metpariyra — Jvomauf

Building the Save Note content

The next thing you’ll do is put together all the composables that you created to make the Save Note screen content.

@Composable
private fun SaveNoteContent(
  note: NoteModel,
  onNoteChange: (NoteModel) -> Unit
) {
  Column(modifier = Modifier.fillMaxSize()) {

  }
}
ContentTextField(
  label = "Title",
  text = note.title,
  onTextChange = { newTitle ->
    onNoteChange.invoke(note.copy(title = newTitle))
  }
)

ContentTextField(
  modifier = Modifier
    .heightIn(max = 240.dp)
    .padding(top = 16.dp),
  label = "Body",
  text = note.content,
  onTextChange = { newContent ->
    onNoteChange.invoke(note.copy(content = newContent))
  }
)
val canBeCheckedOff: Boolean = note.isCheckedOff != null

NoteCheckOption(
  isChecked = canBeCheckedOff,
  onCheckedChange = { canBeCheckedOffNewValue ->
    val isCheckedOff: Boolean? = if (canBeCheckedOffNewValue) false else null
    
    onNoteChange.invoke(note.copy(isCheckedOff = isCheckedOff))
  }
)

PickedColor(color = note.color)
import androidx.compose.material.*
@Preview
@Composable
fun SaveNoteContentPreview() {
  SaveNoteContent(
    note = NoteModel(title = "Title", content = "content"),
    onNoteChange = {}
  )
}
Content Composable — Preview
Dajvojb Boqkifujbu — Ldaxuem

Wrapping up the Save Note screen

Great job so far! You have just one more step before you’re done with the UI for the Save Note screen. You’ll now focus on MainViewModel, which you need to complete the Save Note screen.

Adding ViewModel support

In MainViewModel, you already added the code to expose the noteEntry state, but you still need to add one more state. In the Save Note screen, the user can choose a color for a note. To display the list of colors the user can choose, you need to provide them to SaveNoteScreen().

val colors: LiveData<List<ColorModel>> by lazy { 
  repository.getAllColors() 
}
import com.raywenderlich.android.jetnotes.domain.model.ColorModel

Changing the noteEntry state

Next, you need to add support for changing the noteEntry state when the user interacts with the Save Note screen.

fun onNoteEntryChange(note: NoteModel) {
  _noteEntry.value = note
}

fun saveNote(note: NoteModel) {
  viewModelScope.launch(Dispatchers.Default) {
    repository.insertNote(note)

    withContext(Dispatchers.Main) {
      JetNotesRouter.navigateTo(Screen.Notes)

      _noteEntry.value = NoteModel()
    }
  }
}

fun moveNoteToTrash(note: NoteModel) {
  viewModelScope.launch(Dispatchers.Default) {
    repository.moveNoteToTrash(note.id)

    withContext(Dispatchers.Main) { 
      JetNotesRouter.navigateTo(Screen.Notes) 
    }
  }
}

Connecting the SaveNoteScreen to the MainViewModel

Now that MainViewModel is ready, you can complete the UI part of the Save Note screen.

Scaffold(
  topBar = {
    val isEditingMode: Boolean = noteEntry.id != NEW_NOTE_ID
    SaveNoteTopAppBar(
      isEditingMode = isEditingMode,
      onBackClick = { 
        JetNotesRouter.navigateTo(Screen.Notes) 
      },
      onSaveNoteClick = { viewModel.saveNote(noteEntry) },
      onOpenColorPickerClick = { },
      onDeleteNoteClick = { 
        viewModel.moveNoteToTrash(noteEntry) 
      }
    )
  },
  bodyContent = { // here
    SaveNoteContent(
      note = noteEntry,
      onNoteChange = { updateNoteEntry ->
        viewModel.onNoteEntryChange(updateNoteEntry)
      }
    )
  }
)
Save Note screen
Cohu Yuzu bvruip

Changing the note’s color

There is still one thing missing: You still can’t change the color of the notes. To fix that, update SaveNoteScreen() like this:

@Composable
fun SaveNoteScreen(viewModel: MainViewModel) {

  ...

  val colors: List<ColorModel> by viewModel.colors
    .observeAsState(listOf())

  val bottomDrawerState: BottomDrawerState =
    rememberBottomDrawerState(BottomDrawerValue.Closed)

  Scaffold(
    topBar = {
      val isEditingMode: Boolean = noteEntry.id != NEW_NOTE_ID
      SaveNoteTopAppBar(
        ...,
        onOpenColorPickerClick = { bottomDrawerState.open() },
        ...
      )
    },
    bodyContent = {
      BottomDrawerLayout(
        drawerState = bottomDrawerState,
        drawerContent = {
          ColorPicker(
            colors = colors,
            onColorSelect = { color ->
              val newNoteEntry = noteEntry.copy(color = color)
              viewModel.onNoteEntryChange(newNoteEntry)
            }
          )
        },
        bodyContent = {
          SaveNoteContent(
            note = noteEntry,
            onNoteChange = { updateNoteEntry ->
              viewModel.onNoteEntryChange(updateNoteEntry)
            }
          )
        }
      )
    }
  )
}
Color picker on Save Note screen
Xexok wozlev od Maye Hulo wryoid

Confirming a delete action

While the Save Note screen is now functionally complete, it’s always nice to pay attention to the details.

val moveNoteToTrashDialogShownState: MutableState<Boolean> = savedInstanceState { false }
SaveNoteTopAppBar(
  ...,
  onDeleteNoteClick = { 
    moveNoteToTrashDialogShownState.value = true 
  }
)
Scaffold(
  topBar = { ... },
  bodyContent = {
    BottomDrawerLayout(...)

    if (moveNoteToTrashDialogShownState.value) {
      AlertDialog(
        onDismissRequest = { 
          moveNoteToTrashDialogShownState.value = false 
        },
        title = {
          Text("Move note to the trash?")
        },
        text = { 
          Text(
            "Are you sure you want to " +
                "move this note to the trash?"
          ) 
        },
        confirmButton = {
          TextButton(onClick = {
            viewModel.moveNoteToTrash(noteEntry)
          }) {
            Text("Confirm")
          }
        },
        dismissButton = {
          TextButton(onClick = {
            moveNoteToTrashDialogShownState.value = false
          }) {
            Text("Dismiss")
          }
        }
      )
    }
  }
)
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.savedinstancestate.savedInstanceState
Alert dialog in Save Note screen
Evikr daehis iv Dito Topu tvbaiw

Adding support for the Back button

Currently, when you open the Save Note screen and press the Back button, the app closes. Since you’re not using activities or fragments that operate on back stacks and handle basic system navigation internally, you need to handle how your app behaves if the user presses the system back button.

setContent {
  Providers(BackPressedDispatcher provides this) {
    JetNotesTheme {
      MainActivityScreen(viewModel = viewModel)
    }
  }
}
import androidx.compose.runtime.Providers
import com.raywenderlich.android.jetnotes.util.BackPressedDispatcher
BackPressHandler(onBackPressed = {
  if (bottomDrawerState.isOpen) {
    bottomDrawerState.close()
  } else {
    JetNotesRouter.navigateTo(Screen.Notes)
  }
})
import com.raywenderlich.android.jetnotes.util.BackPressHandler

Using Material Design composables in the Notes screen

The Material Design composables that Jetpack Compose provides are all built with basic composables. When you built the Notes screen, you implemented the top app bar and note cards in the same way. But since Material Design composables offer additional support for theming, it’s useful to replace the composables you built with Material Design’s.

Scaffold(
  topBar = {
    TopAppBar(
      title = {
        Text(
          text = "JetNotes",
          color = MaterialTheme.colors.onPrimary
        )
      },
      navigationIcon = {
        IconButton(onClick = {
          scaffoldState.drawerState.open()
        }) {
          Icon(imageVector = Icons.Filled.List)
        }
      }
    )
  },
...
)
import androidx.compose.material.TopAppBar

Using a Material composable for Note

There’s one more thing you can replace with Material Design composables: your Note().

Card(
  shape = RoundedCornerShape(4.dp),
  modifier = Modifier
    .padding(8.dp)
    .fillMaxWidth(),
  backgroundColor = MaterialTheme.colors.surface
) {
  ListItem(
    text = { Text(text = note.title, maxLines = 1) },
    secondaryText = { Text(text = note.content, maxLines = 1) },
    icon = {
      NoteColor(
        color = Color.fromHex(note.color.hex),
        size = 40.dp,
        border = 1.dp
      )
    },
    trailing = {
      if (note.isCheckedOff != null) {
        Checkbox(
          checked = note.isCheckedOff,
          onCheckedChange = { isChecked ->
            val newNote = note.copy(isCheckedOff = isChecked)
            onNoteCheckedChange.invoke(newNote)
          },
          modifier = Modifier.padding(start = 8.dp)
        )
      }
    },
    modifier = Modifier.clickable { onNoteClick.invoke(note) })
}
import androidx.compose.material.Card
import androidx.compose.material.ListItem
import androidx.compose.material.*

Theming in Compose

Every Android app has a specific color palette, typography and shapes. Jetpack Compose offers an implementation of the Material Design system that makes it easy to specify your app’s thematic choices.

private val LightThemeColors = lightColors(
  primary = rwGreen,
  primaryVariant = rwGreenDark,
  secondary = rwRed
)

private val DarkThemeColors = lightColors(
  primary = rwGreen,
  primaryVariant = rwGreenDark,
  secondary = rwRed
)

@Composable
fun JetNotesTheme(content: @Composable () -> Unit) {
  val isDarkThemeEnabled = 
    isSystemInDarkTheme() || JetNotesThemeSettings.isDarkThemeEnabled
  
  val colors = if (isDarkThemeEnabled) DarkThemeColors else LightThemeColors

  MaterialTheme(colors = colors, content = content)
}
private val DarkThemeColors = darkColors(
  primary = Color(0xFF00A055),
  primaryVariant = Color(0xFF00F884),
  secondary = rwRed,
  onPrimary = Color.White,
)
import androidx.compose.material.darkColors
import androidx.compose.ui.graphics.Color
Dark Theme
Soxq Xciyu

Key points

  • Jetpack Compose provides composables that make it easy to follow Material Design.
  • With remember(), Compose lets you store values in the composition tree.
  • Using the OnBackPressedDispatcherOwner and providing it through an Ambient, you gain access to system back button handling.
  • Jetpack Compose offers a Material Design implementation that allows you to theme your app by specifying the color palette, typography and shapes.
  • Using MaterialTheme(), you define a theme for your app, that customizes colors, typography and shapes.
  • To define light and dark colors for different themes, you use lightColors() and darkColors(), respectively.

Where to go from here?

Hopefully, this was a fun ride for you. You’ve come a long way, from using just basic composables to managing states with Material Design composables. In the next section, you’ll work on a more complex app, JetReddit! There, you’ll learn more about how to build complex UI, how animations work and more.

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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now