Recomposition

Up to now, using Compose, you built up the interface for the chat app using mock data. It’s like you made the yummy-looking but completely fake cake some stores display. But you want to have your cake and eat it too! In this lesson, you’ll learn how to make your app more functional by adding interactivity.

You need to do two main things to make the chat interface interactive. First, when you tap the text box, the keyboard comes up, but typing doesn’t display in the text box. So, you’ll need to make the text that’s typed appear in the text box. Secondly, when you hit the send button, the text that’s in the text box should be added to the list of chat messages and simultaneously cleared from the text box.

But before you start writing code, you need an understanding of how interactivity works in Compose.

State

To make any app functional, you must know how to manage state. At its core, every app works with specific values that can change. For example, in Kodeco chat, a user can:

  • Add a new chat message.
  • Delete a chat message.
  • Upload an image attachment to a chat message.

State is any value that can change over time. Those values can include anything from a database entry to a class property. As the state changes, it’s crucial that the UI accurately reflects that state, so you’ll need to update UI when the state changes.

Compose is declarative, so the only way to update it is by calling the same composable with new arguments. These arguments are representations of the UI state. Any time a state is updated, a recomposition occurs.

In Conversation.kt, look at the code for SimpleUserInput():

@Composable
fun SimpleUserInput() {
  val context = LocalContext.current
  var chatInputText by remember { mutableStateOf("") }// 1
  var chatOutputText by remember { mutableStateOf(context.getString(R.string.chat_display_default)) }
  Text(text = chatOutputText)

  Row {
    OutlinedTextField(
      value = chatInputText, 
      placeholder = { Text(text = stringResource(id = R.string.chat_entry_default)) }, // 2
      onValueChange = { //3
      },
    )

    Button(onClick = {
    }) {
      Text(text = stringResource(id = R.string.send_button))
    }

  }
}
  // ...
  1. Note the use of by remember. Composable functions use the remember API to store an object in memory and update it during recomposition. In other words, you use remember to store the state of a composable. A composable that uses remember to store an object creates internal state, making the composable stateful. This can be useful when you have simple composables that you want to manage their own state. But these are also less reusable and harder to test.
  2. Initially, the value of the chat input text box is set to the placeholder text, “Type your text here”.
  3. When you input text into a text field, it’s important for the displayed value to reflect your input in real time. This is where you’ll use onValueChange. Every time you type a character into the text field, this composable gets recomposed. In other words, the state of the text field updates. Update the code of the body of onValueChange, like so:
onValueChange = {
  chatInputText = it
},

Now, when you select the chat input text box, and you type a letter on the keyboard, the value of the chat input text gets updated with it, which, in this context, is the string that was typed into the box.

Build and run. Type something into the chat input text box.

Voila! Now, when you type, you see the value updated in the input box.

State Hoisting

A stateless composable is a composable that doesn’t hold any state. An easy way to achieve stateless-ness is by using state hoisting. You’ve actually been using this already. Again:

BasicTextField(
  value = textFieldValue,
  onValueChange = { onTextChanged(it) }
)

State hoisting is a programming pattern in which you move state to the caller of a composable by replacing internal state in a composable with a parameter and events.

For composables, this often means introducing two parameters to the composable:

  • value: T: The current value to display.
  • onValueChange: (T) ▸ Unit: An event that requests a change to a value, where (T) represents providing a new value.

The token T represents a generic type that depends on the data and the UI you’re showing. If you look at the parameters of UserInputText again, you see that you follow the same approach for your state and events. In that case, your T is a TextFieldValue.

By applying state hoisting to a composable, you make it stateless — which means it can’t hold any state. Stateless composables are easier to test, have fewer bugs, and offer more reuse opportunities.

Unidirectional Data Flow

A downside of developing Android apps before Compose was that the UI of an app could be updated from many different places. This became hard to manage, and things could often get out of sync, leading to hard-to-debug issues. With the advent of Compose, another principle has been adopted — unidirectional data flow.

In unidirectional data flow, both the state changes and UI updates have only one direction. This means that the state change events can come from only one source, usually user interactions, and UI updates can come only from the state manager. Compose was based on the concept of decoupling components that display state in the UI from the app parts that store and change state.

Another key concept is that the UI observes the state. Every time there’s a new state, the UI recomposes to display it. Android provides some very handy Android Architecture Components libraries to help with this. For the state manager, there’s the ViewModel. And for observing data in a unidirectional manner, there’s Flow.

We won’t get into implementing ViewModel and Flow here, but it’s good to know why Compose is designed this way.

Next up in the video demo, you’ll complete the interactivity by enabling the send button!

See forum comments
Download course materials from Github
Previous: Introduction Next: Demo