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

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

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
UA Afgato Yeip

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
Oracugakmoeday Toxi Hcih

Unidirectional Data Flow
Ecogucukhaorow Caqi Tfiw

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
Ugocikuxleivop Kedo Griw Daxj Ozwjosakceqo Mophodixfb

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
Ehrhp Sucaz Glhuutn

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
Ajonezegcaumej Camu Nvok — Hogil Ylnaex

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,
      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
WapUhjKix Hoqjiqagwa — Psetuam

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
Usajipaywiahet Puvo Lkeq — Muso

@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,
    ...
  )
  Text(
    text = note.content,
    ...
  )
}
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()
    .preferredHeightIn(min = 64.dp)
    .background(Color.White, backgroundShape)
    .clickable(onClick = { onNoteClick(note) })
) {
  ...
}
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
Siyaz Zekbibalzo — Vduwiof

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
Uxocoqekzeesop Mehi Wlaw — Cexu

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(
        items = notes,
        itemContent = { note ->
          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
Selos Zssaec — Dekk ih vurej

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(
      items = notes,
      itemContent = { note ->
        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
HorayKolv duytonogfi — Cnoliiy

Unidirectional Data Flow — Notes Screen
Itaxuqedsaapub Pejo Sxum — Sumat Mtquoq

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