Chapters

Hide chapters

Jetpack Compose by Tutorials

Second Edition · Android 13 · Kotlin 1.7 · Android Studio Dolphin

Section VI: Appendices

Section 6: 1 chapter
Show chapters Hide chapters

8. Applying Material Design to Compose
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

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.
  • Learn more about Jetpack Compose navigation API.
  • 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 can 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 learn more about the composable that enables you to have the following layout structure.

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.

Using Scaffold

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

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

  Scaffold(
    scaffoldState = scaffoldState,
    drawerContent = {
      AppDrawer(
        currentScreen = Screen.Notes,
        onScreenSelected = { screen ->
          coroutineScope.launch {
            scaffoldState.drawerState.close()
          }
        }
      )
    },
    content = {
      NavHost(
        navController = navController,
        startDestination = Screen.Notes.route
      ) {
        composable(Screen.Notes.route) {
          NotesScreen(viewModel = viewModel)
        }
      }
    }
  )
}
Notes Screen and App Drawer
Jacuv Ltzuun elw Idv Mkideg

@Composable
fun Scaffold(
  modifier: Modifier = Modifier,
  scaffoldState: ScaffoldState = rememberScaffoldState(),
  topBar: @Composable () -> Unit = {},
  bottomBar: @Composable () -> Unit = {},
  snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
  floatingActionButton: @Composable () -> Unit = {},
  floatingActionButtonPosition: FabPosition = FabPosition.End,
  isFloatingActionButtonDocked: Boolean = false,
  drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
  drawerGesturesEnabled: Boolean = true,
  drawerShape: Shape = MaterialTheme.shapes.large,
  drawerElevation: Dp = DrawerDefaults.Elevation,
  drawerBackgroundColor: Color = MaterialTheme.colors.surface,
  drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
  drawerScrimColor: Color = DrawerDefaults.scrimColor,
  backgroundColor: Color = MaterialTheme.colors.background,
  contentColor: Color = contentColorFor(backgroundColor),
  content: @Composable (PaddingValues) -> Unit
)

Adding Scaffold to Notes Screen

As you just learned, Scaffold() allows you to add app drawer content. It also lets the user pull the drawer out by dragging it from the left side of the screen. Next, you are going to use it for top bar as well. Open NotesScreen.kt and replace Column() with Scaffold(). Also, notice that with the following code snippet you’ll add one more parameter to the NotesScreen():

@Composable
fun NotesScreen(
  viewModel: MainViewModel,
  onOpenNavigationDrawer: () -> Unit = {} // Add code here
) {

  // Observing notes state from MainViewModel
  ...

  // Add code below here

  val scaffoldState: ScaffoldState = rememberScaffoldState()

  Scaffold(
    scaffoldState = scaffoldState,
    topBar = {
      TopAppBar(
        title = "JetNotes",
        icon = Icons.Filled.List,
        onIconClick = { onOpenNavigationDrawer.invoke() }
      )
    },
    content = {
      if (notes.isNotEmpty()) {
        NotesList(
          notes = notes,
          onNoteCheckedChange = {
            viewModel.onNoteCheckedChange(it)
          },
          onNoteClick = { viewModel.onNoteClick(it) }
        )
      }
    }
  )
}
import androidx.compose.material.Scaffold
import androidx.compose.material.ScaffoldState
import androidx.compose.material.rememberScaffoldState
JetNotesTheme {
  ...

  Scaffold(
    ...
    content = {
      NavHost(
        navController = navController,
        startDestination = Screen.Notes.route
      ) {
        composable(Screen.Notes.route) {
          NotesScreen(
            viewModel = viewModel,
            onOpenNavigationDrawer = {            // add code here
              coroutineScope.launch {
                scaffoldState.drawerState.open()
              }
            }
          )   
        }
      }
    }
  )
}
Notes Screen
Jaroh Fbwiuz

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
inline fun <T> remember(calculation: @DisallowComposableCalls () -> 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 {
  return rememberSaveable(saver = DrawerState.Saver(confirmStateChange)) {
      DrawerState(initialValue, confirmStateChange)
  }
}

remember’s Effect on the Composition Tree

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

Notes Screen - Composition Tree
Dilub Fxgeuh - Bogxapaqeuh Vveu

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,
  onOpenNavigationDrawer: () -> Unit = {},
  onNavigateToSaveNote: () -> Unit = {}          // add code here
) {

  // Observing notes state from MainViewModel
  ...

  val scaffoldState: ScaffoldState = rememberScaffoldState()

  Scaffold(
    ...,
    floatingActionButtonPosition = FabPosition.End,
    floatingActionButton = {
      FloatingActionButton(
        onClick = {
          viewModel.onCreateNewNoteClick()
          onNavigateToSaveNote.invoke()
        },
        contentColor = MaterialTheme.colors.background,
        content = {
          Icon(
            imageVector = Icons.Filled.Add,
            contentDescription = "Add Note Button"
          )
        }
      )
    },
    ...
  )
}
import androidx.compose.material.*
import androidx.compose.material.icons.filled.Add
Notes Screen with Floating Action Button
Yikaw Qsnoag yatz Bteinudv Ubneef Kogvab

Adding an Entry Point to Save Note Screen

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,
  onNavigateBack: () -> Unit = {}
) {

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

Using Jetpack Compose Navigation API 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 based on navController state.

@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun MainActivityScreen(
  navController: NavHostController,
  viewModel: MainViewModel,
  openNavigationDrawer: () -> Unit
) {
  NavHost(
    navController = navController,
    startDestination = Screen.Notes.route
  ) {
    composable(Screen.Notes.route) {
      NotesScreen(
        viewModel,
        openNavigationDrawer,
        { navController.navigate(Screen.SaveNote.route) }
      )
    }
    composable(Screen.SaveNote.route) {
      SaveNoteScreen(
        viewModel,
        { navController.popBackStack() }
      )
    }
    composable(Screen.Trash.route) {
      TrashScreen(viewModel, openNavigationDrawer)
    }
  }
}
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.Composable
import com.yourcompany.android.jetnotes.ui.screens.SaveNoteScreen
import com.yourcompany.android.jetnotes.ui.screens.TrashScreen
import androidx.navigation.NavHostController

Connecting MainActivityScreen to MainActivity

Next, you’ll connect this composable to MainActivity, but also connect your app drawer with the navigation graph.

...

val navBackStackEntry                                   // add code here
  by navController.currentBackStackEntryAsState()

Scaffold(
  scaffoldState = scaffoldState,
  drawerContent = {
    AppDrawer(                                          // add code here
      currentScreen = Screen.fromRoute(
        navBackStackEntry?.destination?.route
      ),
      onScreenSelected = { screen ->
        navController.navigate(screen.route) {
          // Pop up to start destination to avoid building the
          // stack for every screen selection
          popUpTo(
            navController.graph.findStartDestination().id
          ) {
            saveState = true
          }

          // Prevent copies of the same destination when screen
          // is reselected
          launchSingleTop = true

          // Restore state when selecting previously selected
          // screen
          restoreState = true
        }
        coroutineScope.launch {
          scaffoldState.drawerState.close()
        }
      }
    )
  },
  content = {
    ...
  }
)
Scaffold(
  ...
  content = {
    MainActivityScreen(
      navController = navController,
      viewModel = viewModel,
      openNavigationDrawer = {
        coroutineScope.launch {
          scaffoldState.drawerState.open()
        }
      }
    )
  }
)
import androidx.compose.runtime.getValue
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
Opening Save Notes Screen
Oyacadq Jahe Bijok Kkhaen

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,
  onNavigateBack: () -> Unit = {}
) {
  Scaffold(
    topBar = {},
    content = {}
  )
}
import androidx.compose.material.Scaffold
Save Note Screen: Top Bar
Viku Vama Jnrair: Muz Yiy

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.Default.ArrowBack,
      contentDescription = "Save Note Button",
      tint = MaterialTheme.colors.onPrimary
    )
  }
}
actions = {
  // Save note action icon
  IconButton(onClick = onSaveNoteClick) {
    Icon(
      imageVector = Icons.Default.Check,
      tint = MaterialTheme.colors.onPrimary,
      contentDescription = "Save Note"
    )
  }

  // Open color picker action icon
  IconButton(onClick = onOpenColorPickerClick) {
    Icon(
      painter = painterResource(
        id = R.drawable.ic_baseline_color_lens_24
      ),
      contentDescription = "Open Color Picker Button",
      tint = MaterialTheme.colors.onPrimary
    )
  }
}
// Delete action icon (show only in editing mode)
if (isEditingMode) {
  IconButton(onClick = onDeleteNoteClick) {
    Icon(
      imageVector = Icons.Default.Delete,
      contentDescription = "Delete Note Button",
      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.painterResource
import com.yourcompany.android.jetnotes.R
@Preview
@Composable
fun SaveNoteTopAppBarPreview() {
  SaveNoteTopAppBar(
    isEditingMode = true,
    onBackClick = {},
    onSaveNoteClick = {},
    onOpenColorPickerClick = {},
    onDeleteNoteClick = {}
  )
}
SaveNoteTopAppBar Composable (Editing Mode) — Preview
XoxeZoloLixEvrRih Xekdemazdo (Evohegl Xiku) — Jgedeaf

SaveNoteTopAppBar Composable (New Note Mode) — Preview
SukaKijiMosAsjWov Rozwibefwi (Kuw Gisa Biva) — Mfahuej

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 = MutableStateFlow<NoteModel>(NoteModel())
val noteEntry: LiveData<NoteModel> = _noteEntry.asLiveData()
@Composable
fun SaveNoteScreen(
  viewModel: MainViewModel,
  onNavigateBack: () -> Unit = {}
) {
  val noteEntry: NoteModel by viewModel.noteEntry
    .observeAsState(NoteModel())

  Scaffold(
    topBar = {
      val isEditingMode: Boolean = noteEntry.id != NEW_NOTE_ID
      SaveNoteTopAppBar(
        isEditingMode = isEditingMode,
        onBackClick = {
          onNavigateBack.invoke()
        },
        onSaveNoteClick = { },
        onOpenColorPickerClick = { },
        onDeleteNoteClick = { }
      ) },
    content = { }
  )
}
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import com.yourcompany.android.jetnotes.domain.model.NEW_NOTE_ID
import com.yourcompany.android.jetnotes.domain.model.NoteModel
Adding the Top Bar to the Save Note Screen
Odtidq nyi Bel Kag do rno Yowe Heli Gfzuav

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()
}

fun onNoteClick(note: NoteModel) {
  _noteEntry.value = note
}
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun NotesScreen(
  viewModel: MainViewModel,
  onOpenNavigationDrawer: () -> Unit,
  onNavigateToSaveNote: () -> Unit = {}
) {

  ...

  Scaffold(
    ...
    content = {
      if (notes.isNotEmpty()) {
        NotesList(
          notes = notes,
          onNoteCheckedChange = {
            viewModel.onNoteCheckedChange(it)
          },
          onNoteClick = {
            viewModel.onNoteClick(it)
            onNavigateToSaveNote.invoke()           // add code here
          }
        )
      }
    }
  )
}
Save Note Screen in Edit Mode
Sota Gozi Vnvauq it Edel Nuve

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 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
BeqpajGememQitpokowp Morcuqehma — Tsiteuf

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 a note has been completed. Users need to mark notes as checkable if they want that feature. Your next step is to give them the 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)
        .align(Alignment.CenterVertically)
    )
    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
WixToJvotkexUmbFahhasifd Walbekoyle — Khehuoq

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),
    colors = TextFieldDefaults.textFieldColors(
      backgroundColor = MaterialTheme.colors.surface
    )
  )
}
@Preview
@Composable
fun ContentTextFieldPreview() {
  ContentTextField(
    label = "Title",
    text = "",
    onTextChange = {}
  )
}
ContentTextField Composable — Preview
MaqsorfDetkMuovy Jasfizaxmi — Fweciaw

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.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.heightIn
@Preview
@Composable
fun SaveNoteContentPreview() {
  SaveNoteContent(
    note = NoteModel(title = "Title", content = "content"),
    onNoteChange = {}
  )
}
Content Composable — Preview
Xaztabs Vawlexersa — Mqimuay

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().asLiveData()
}
import com.yourcompany.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) {
      _noteEntry.value = NoteModel()
    }
  }
}

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

Connecting the SaveNoteScreen to the MainViewModel

Now that MainViewModel is ready, you can complete the UI part of the Save Note screen. Open SaveNoteScreen.kt and update Scaffold() in SaveNoteScreen():

Scaffold(
  topBar = {
    val isEditingMode: Boolean = noteEntry.id != NEW_NOTE_ID
    SaveNoteTopAppBar(
      isEditingMode = isEditingMode,
      onBackClick = {
        onNavigateBack.invoke()
      },
      onSaveNoteClick = { // add code here
        viewModel.saveNote(noteEntry)
        onNavigateBack.invoke()
      },
      onOpenColorPickerClick = { },
      onDeleteNoteClick = { // add code here
        viewModel.moveNoteToTrash(noteEntry)
        onNavigateBack.invoke()
      }
    )
  },
  content = { // add code here
    SaveNoteContent(
      note = noteEntry,
      onNoteChange = { updateNoteEntry ->
        viewModel.onNoteEntryChange(updateNoteEntry)
      }
    )
  }
)
Save Note Screen
Nezu Vuno Yfwooy

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
@ExperimentalMaterialApi // add code here (BottomDrawer)
fun SaveNoteScreen(
  viewModel: MainViewModel,
  onNavigateBack: () -> Unit = {}
) {

  ...

  // add code here
  val colors: List<ColorModel> by viewModel.colors
    .observeAsState(listOf())

  // add code here
  val bottomDrawerState: BottomDrawerState =
    rememberBottomDrawerState(BottomDrawerValue.Closed)

  // add code here
  val coroutineScope = rememberCoroutineScope()

  Scaffold(
    topBar = {
      val isEditingMode: Boolean = noteEntry.id != NEW_NOTE_ID
      SaveNoteTopAppBar(
        ...,
        onOpenColorPickerClick = { // add code here
          coroutineScope.launch {
            bottomDrawerState.open()
          }
        },
        ...
      )
    },
    content = {
      BottomDrawer( // add code here
        drawerState = bottomDrawerState,
        drawerContent = {
          ColorPicker(
            colors = colors,
            onColorSelect = { color ->
              val newNoteEntry = noteEntry.copy(color = color)
              viewModel.onNoteEntryChange(newNoteEntry)
            }
          )
        },
        content = {
          SaveNoteContent(
            note = noteEntry,
            onNoteChange = { updateNoteEntry ->
              viewModel.onNoteEntryChange(updateNoteEntry)
            }
          )
        }
      )
    }
  )
}
import androidx.compose.runtime.rememberCoroutineScope
Color Picker on Save Note Screen
Yujul Borfon ex Daji Qina Xfmiis

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> = rememberSaveable {
  mutableStateOf(false)
}
SaveNoteTopAppBar(
  ...,
  onDeleteNoteClick = {
    moveNoteToTrashDialogShownState.value = true
  }
)
Scaffold(
  topBar = { ... },
  content = {
    BottomDrawer(...)

    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)
            onNavigateBack.invoke()
          }) {
            Text("Confirm")
          }
        },
        dismissButton = {
          TextButton(onClick = {
            moveNoteToTrashDialogShownState.value = false
          }) {
            Text("Dismiss")
          }
        }
      )
    }
  }
)
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
Alert Dialog in Save Note Screen
Umomx Deodif os Viza Koro Hgxeeq

Using Material Design Composables in the Notes Screen

The Material Design composables that Jetpack Compose provide 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 = {
          onOpenNavigationDrawer.invoke()
        }) {
          Icon(
            imageVector = Icons.Filled.List,
            contentDescription = "Drawer Button"
          )
        }
      }
    )
  },
...
)
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().

val background = if (isSelected)
  Color.LightGray
else
  MaterialTheme.colors.surface

Card(
  shape = RoundedCornerShape(4.dp),
  modifier = modifier
    .padding(8.dp)
    .fillMaxWidth(),
  backgroundColor = background
) {
  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)
    }
  )
}
@Composable
@ExperimentalMaterialApi // here
fun Note(
  modifier: Modifier = Modifier, // here
  note: NoteModel,
  onNoteClick: (NoteModel) -> Unit = {},
  onNoteCheckedChange: (NoteModel) -> Unit = {},
  isSelected: Boolean = false
) {
...
}
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 = green,
  primaryVariant = greenDark,
  secondary = red
)

private val DarkThemeColors = lightColors(
  primary = green,
  primaryVariant = greenDark,
  secondary = red
)

@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 = red,
  onPrimary = Color.White,
)
import androidx.compose.material.darkColors
import androidx.compose.ui.graphics.Color
Dark Theme
Desq Fceba

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 Jetpack Compose navigation allows you to easily navigate between your composables. Navigation is structured around back stack.
  • 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 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