Chapters

Hide chapters

Jetpack Compose by Tutorials

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

7. Managing State in 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.

Great job on completing the first two chapters of this section. Now you know the basic principles of composing a UI and making it beautiful.

In this chapter, you’ll change your focus from the UI of JetNotes to making it functional. To make any app functional, you need to know how to manage state, which is the topic of this chapter.

In this chapter, you’ll learn:

  • What state is.
  • What unidirectional data flow is.
  • How to think about state and events when creating stateless composables.
  • How to use ViewModel and LiveData from Android Architecture Components to manage state in Compose.
  • How to add functionality to the Notes screen.

Get ready to dive in by taking a deeper look at what state is and why it’s critical for your app.

Understanding state

Before you can understand the state management theory, you need to define what state is.

At its core, every app works with specific values that can change. For example, JetNotes manages notes, and users can make changes to the list of notes. They can:

  • Add new notes.
  • Delete current notes.
  • Change a note.
  • Complete a note.

State is any value that can change over time. Those values can include anything from an entry in a database to a property of a class. And as the state changes, you need to update the UI to reflect those changes.

UI update loop

When you think about how users interact with Android apps, you can say that it’s like having a conversation. Users communicate through events like clicking, dragging and speaking while the app responds by displaying the app’s state.

UI Update Loop
UO Itjiwi Waun

Handling state with Android UI Toolkit

Before going further, remind yourself how the current Android UI Toolkit manages state.

Handling state with unidirectional data flow

In the previous Spinner example, the data flow had multiple directions it could come from and multiple directions it could go to, depending on trigger events and UI updates it reflected. This means it’s hard to keep everything in sync and its hard to know where the change is coming from at all times.

Unidirectional Data Flow
Uqapeqofteinoq Yuqo Khal

Unidirectional Data Flow
Asopatabjeepiw Fuli Ybun

Compose & ViewModel

As mentioned in the previous section, in unidirectional data flow, the UI observes the state. The Android framework offers some great Android Architecture Components that make it easy for you to follow that approach, including the ViewModel and LiveData.

Unidirectional Data Flow With Architecture Components
Egoladuppoikuy Quvi Rkak Bewr Iwxtukujfuhi Wihweyojmv

Creating the Notes screen

So far, JetNotes has no screens. The only thing you can do with it at the moment is pull out the app drawer and inspect one note, which you use to track your progress. This is about to change. :]

@Composable
fun NotesScreen(viewModel: MainViewModel) {

}
import androidx.compose.runtime.Composable
import com.raywenderlich.android.jetnotes.viewmodel.MainViewModel
JetNotesTheme {
  NotesScreen(viewModel = viewModel)
}
import com.raywenderlich.android.jetnotes.ui.screens.NotesScreen
Empty Notes Screens
Ayhwd Baled Yvbeowx

Implementing unidirectional data flow

Now that you have an entry point to Notes, you need to implement MainViewModel so it supports unidirectional data flow.

val notesNotInTrash: LiveData<List<NoteModel>> by lazy {
  repository.getAllNotesNotInTrash()
}
import androidx.lifecycle.LiveData
import com.raywenderlich.android.jetnotes.domain.model.NoteModel
fun onCreateNewNoteClick() {
  // TODO - Open SaveNoteScreen
}

fun onNoteClick(note: NoteModel) {
  // TODO - Open SaveNoteScreen in Edit mode
}

fun onNoteCheckedChange(note: NoteModel) {
  viewModelScope.launch(Dispatchers.Default) {
  	repository.insertNote(note)
  }
}
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Unidirectional Data Flow — Notes Screen
Oxunicovxuegos Gafi Pgoh — Petoq Wvfuoz

Creating the app bar

Before connecting the NotesScreen to the MainViewModel, you need to implement the UI components that make up the Notes screen.

@Composable
fun TopAppBar(
  title: String,
  icon: ImageVector,
  onIconClick: () -> Unit,
) {
  Row(
    modifier = Modifier
      .fillMaxWidth()
      .height(56.dp)
      .background(color = MaterialTheme.colors.primarySurface)
  ) {
    Image(
      imageVector = icon,
      contentDescription = "Top App Bar Icon",
      colorFilter = ColorFilter
        .tint(MaterialTheme.colors.onPrimary),
      modifier = Modifier
        .clickable(onClick = onIconClick)
        .padding(16.dp)
        .align(Alignment.CenterVertically)
    )
    Text(
      text = title,
      color = MaterialTheme.colors.onPrimary,
      style = TextStyle(
        fontWeight = FontWeight.Medium,
        fontSize = 20.sp,
        letterSpacing = 0.15.sp
      ),
      modifier = Modifier
        .fillMaxWidth()
        .align(Alignment.CenterVertically)
        .padding(start = 16.dp, end = 16.dp)
    )
  }
}
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.primarySurface
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.graphics.vector.ImageVector
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Preview
@Composable
private fun TopAppBarPreview() {
  JetNotesTheme {
    TopAppBar(
      title = "JetNotes",
      icon = Icons.Filled.List,
      onIconClick = {}
    )
  }
}
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.List
import androidx.compose.ui.tooling.preview.Preview
import com.raywenderlich.android.jetnotes.theme.JetNotesTheme
TopAppBar Composable — Preview
GefOsrTuk Loxdesehja — Mzejiot

Stateless composables

In MainViewModel, you exposed the list of NoteModels as a state, but your Note() still isn’t ready to render a specific NoteModel.

Unidirectional Data Flow — Note
Egojabarbiumot Befu Pjir — Hapu

@Composable
fun Note(
  note: NoteModel,
  onNoteClick: (NoteModel) -> Unit = {},
  onNoteCheckedChange: (NoteModel) -> Unit = {}
) {
	// ...
}
import com.raywenderlich.android.jetnotes.domain.model.NoteModel

Rendering NoteModel’s state

To render the NoteModel in Note(), you need to replace your current, hard-coded values with the values from NoteModel.

NoteColor(
  modifier = Modifier
    .align(Alignment.CenterVertically)
    .padding(start = 16.dp, end = 16.dp),
  color = Color.fromHex(note.color.hex),
  size = 40.dp,
  border = 1.dp
)
import com.raywenderlich.android.jetnotes.util.fromHex
Column(
  modifier = Modifier
    .weight(1f)
    .align(Alignment.CenterVertically)
) {
  Text(
    text = note.title, // here
    ...
  )
  Text(
    text = note.content, // here
    ...
  )
}
if (note.isCheckedOff != null) {
  Checkbox(
    checked = note.isCheckedOff,
    onCheckedChange = {},
    modifier = Modifier
      .padding(16.dp)
      .align(Alignment.CenterVertically)
  )
}

Passing up Note events

Remember, the first of the two events that a note can pass up to a parent is when a user clicks the note. You’ll handle that first, by updating the Row modifier to allow that:

Row(
  modifier = Modifier
    .padding(8.dp)
    .shadow(1.dp, backgroundShape)
    .fillMaxWidth()
    .heightIn(min = 64.dp)
    .background(Color.White, backgroundShape)
    .clickable(onClick = { onNoteClick(note) }) // here
) {
  ...
}
import androidx.compose.foundation.clickable
Checkbox(
  checked = note.isCheckedOff,
  onCheckedChange = { isChecked -> // here
    val newNote = note.copy(isCheckedOff = isChecked)
    onNoteCheckedChange(newNote)
  },
  modifier = Modifier
    .padding(16.dp)
    .align(Alignment.CenterVertically)
)
@Preview
@Composable
private fun NotePreview() {
  Note(note = NoteModel(1, "Note 1", "Content 1", null))
}
Notes Composable — Preview
Mimup Govvisafdo — Lgucouv

Unidirectional data flow with stateless composables

Hoisting the state out of Note() has some advantages: It’s now easier to reason about the composable, reuse it in different situations and to test it. Plus, now that you’ve decoupled Note() from how you store the state, if you modify or replace MainViewModel, you don’t have to change how you implement Note().

Unidirectional Data Flow — Note
Usuragopcuukeb Pave Vvud — Nane

Displaying notes in the Notes screen

Now that Note is stateless, you’re ready to display notes in the Notes screen.

@Composable
fun NotesScreen(viewModel: MainViewModel) {

  val notes: List<NoteModel> by viewModel
    .notesNotInTrash
    .observeAsState(listOf())

  Column {
    TopAppBar(
      title = "JetNotes",
      icon = Icons.Filled.List,
      onIconClick = {}
    )
    LazyColumn {
      items(count = notes.size) { noteIndex ->
        val note = notes[noteIndex]
        Note(
          note = note,
          onNoteClick = {
            viewModel.onNoteClick(it)
          },
          onNoteCheckedChange = {
            viewModel.onNoteCheckedChange(it)
          }
        )
      }
    }
  }
}
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.List
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import com.raywenderlich.android.jetnotes.domain.model.NoteModel
import com.raywenderlich.android.jetnotes.ui.components.Note
import com.raywenderlich.android.jetnotes.ui.components.TopAppBar
Notes Screen — List of notes
Ceyit Xkdeig — Ziyf iq sewos

Extracting a stateless composable

Look at NotesScreen() code and you’ll see it has a dependency on the final class, MainViewModel, which directly changes notesNotInTrash’s state. That makes it a stateful composable.

@Composable
private fun NotesList(
  notes: List<NoteModel>,
  onNoteCheckedChange: (NoteModel) -> Unit,
  onNoteClick: (NoteModel) -> Unit
) {
  LazyColumn {
    items(count = notes.size) { noteIndex ->
      val note = notes[noteIndex]
      Note(
        note = note,
        onNoteClick = onNoteClick,
        onNoteCheckedChange = onNoteCheckedChange
      )
    }
  }
}

@Preview
@Composable
private fun NotesListPreview() {
  NotesList(
    notes = listOf(
      NoteModel(1, "Note 1", "Content 1", null),
      NoteModel(2, "Note 2", "Content 2", false),
      NoteModel(3, "Note 3", "Content 3", true)
    ),
    onNoteCheckedChange = {},
    onNoteClick = {}
  )
}
import androidx.compose.ui.tooling.preview.Preview
Column {
  TopAppBar(
    title = "JetNotes",
    icon = Icons.Filled.List,
    onIconClick = {}
  )
  NotesList( // here
    notes = notes,
    onNoteCheckedChange = { viewModel.onNoteCheckedChange(it) },
    onNoteClick = { viewModel.onNoteClick(it) }
  )
}
NotesList Composable — Preview
XuhasGerm Himceqanwa — Hxedook

Unidirectional Data Flow — Notes Screen
Oleniqowfeotok Rumo Spac — Peduq Qfcuub

Key points

  • State is any value that can change over time.
  • The UI update loop is made of three key concepts: event, update state and display state.
  • Unidirectional data flow is a design where state flows down and events flow up.
  • You can use the Android Architecture Components, ViewModel and LiveData, to implement unidirectional data flow in Compose.
  • A ViewModel lets you extract state from the UI and define events that the UI can call to update that state.
  • LiveData allows you to create observable state holders.
  • A stateless composable is a composable that cannot change any state itself.
  • State hoisting is a programming pattern where you move state to the caller of a composable by replacing internal state in that composable with a parameter and events.
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