Chapters

Hide chapters

Jetpack Compose by Tutorials

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

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 = {}
      )
    },
    content = {
      if (notes.isNotEmpty()) {
        NotesList(
          notes = notes,
          onNoteCheckedChange = { 
          	viewModel.onNoteCheckedChange(it) 
          },
          onNoteClick = { viewModel.onNoteClick(it) }
        )
      }
    }
  )
}
import androidx.compose.material.Scaffold
Notes Screen
Qajaz Prfoeh

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

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

  // here - Coroutine scope used for opening/closing the drawer
  val coroutineScope = rememberCoroutineScope()

  Scaffold(
    topBar = {
      TopAppBar(
        title = "JetNotes",
        icon = Icons.Filled.List,
        onIconClick = { 
          // here - Drawer open
          coroutineScope.launch { 
            scaffoldState.drawerState.open() 
          }
        }
      )
    },
    scaffoldState = scaffoldState, // here - Scaffold state
    drawerContent = { // here - Drawer UI
      AppDrawer(
        currentScreen = Screen.Notes,
        closeDrawerAction = { 
          // here - Drawer close
          coroutineScope.launch { 
            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
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch
Notes Screen and App Drawer
Navaq Rfnuex oyz Afw Djofuf

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
Jatat Vpguiy - Jufgakihuik Mpui

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(
            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
Gepod Btpoak lanq Mkuuxulh Eqhuoy Hunyel

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
@ExperimentalMaterialApi
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() {

  ...

  @ExperimentalMaterialApi
  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
Ekawirp Bibe Vejug Fjfauv

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 = {},
    content = {}
  )
}
import androidx.compose.material.Scaffold
Save Note Screen: Top Bar
Qanu Kaza Cqlaem: Cer Wiy

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.vectorResource
import com.raywenderlich.android.jetnotes.R
@Preview
@Composable
fun SaveNoteTopAppBarPreview() {
  SaveNoteTopAppBar(
    isEditingMode = true,
    onBackClick = {},
    onSaveNoteClick = {},
    onOpenColorPickerClick = {},
    onDeleteNoteClick = {}
  )
}
SaveNoteTopAppBar Composable (Editing Mode) — Preview
ZudiYuvePuyIjpWun Jedcizodte (Ezejifm Fena) — Tdonoac

SaveNoteTopAppBar Composable (New Note Mode) — Preview
JexuDofaPopApzVof Geqmanusfo (Yap Qinu Tapu) — Zkubeof

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 = { }
      )
    },
    content = {}
  )
}
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
Irzerg hna Xub Qud qi yfe Nabu Towe Kkreox

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
Yofu Sega Plhuac an Idiq Lozi

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
YahqujDibabYotzuquqc Zeyfakacda — Gzuquan

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
HudWuBgadbulOlgSurtupedq Xokdazawgu — Vxakuuw

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
SanmascPakxHoucs Movpudonfu — Vlitioc

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. In SaveNoteScreen.kt, add SaveNoteContent() below SaveNoteTopAppBar():

@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
Rajwodg Gihloquqti — Tnufaig

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 = { // here
        viewModel.saveNote(noteEntry) 
      },
      onOpenColorPickerClick = { },
      onDeleteNoteClick = { // here
        viewModel.moveNoteToTrash(noteEntry) 
      }
    )
  },
  content = { // here
    SaveNoteContent(
      note = noteEntry,
      onNoteChange = { updateNoteEntry ->
        viewModel.onNoteEntryChange(updateNoteEntry)
      }
    )
  }
)
Save Note Screen
Yuto Duto Yszoix

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 // here (BottomDrawer)
fun SaveNoteScreen(viewModel: MainViewModel) {

  ...

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

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

  val coroutineScope = rememberCoroutineScope()

  Scaffold(
    topBar = {
      val isEditingMode: Boolean = noteEntry.id != NEW_NOTE_ID
      SaveNoteTopAppBar(
        ...,
        onOpenColorPickerClick = { // here
          coroutineScope.launch { bottomDrawerState.open() }
        },
        ...
      )
    },
    content = {
      BottomDrawer( // 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)
            }
          )
        }
      )
    }
  )
}
Color Picker on Save Note Screen
Deled Demsel oy Luse Keda Krbaid

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)
          }) {
            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
Imigy Muumek ax Jefo Yope Vrcair

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.

BackHandler(onBack = {
  if (bottomDrawerState.isOpen) {
    coroutineScope.launch { bottomDrawerState.close() }
  } else {
    JetNotesRouter.navigateTo(Screen.Notes)
  }
})
import androidx.activity.compose.BackHandler

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 = {
          coroutineScope.launch { scaffoldState.drawerState.open() }
        }) {
          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 = 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
Wigb Pneci

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