Unidirectional data flow in Jetpack Compose (Android Jetpack Compose)

Mahmoud Alkateb
3 min readJun 10, 2023

--

Unidirectional Data Flow (UDF), also known as State Hoisting, is a prevalent architectural pattern where the state descends and the events ascend, leading to a uni-directional stream of information. Jetpack Compose employs this methodology to segregate composables, or components responsible for UI state representation, from those that manage state storage and modification.

In a UDF configured system, the user interaction loop proceeds as follows:

  1. Event Emission: Any event, be it a user-triggered UI event such as a button click, or system-generated alerts like a session timeout notification, is sent upwards for handling. This could typically be passed on to a ViewModel or another handler.
  2. State Update: As a reaction to these events, an event handler can modify the state of the system.
  3. State Reflection: Post-modification, the updated state is relayed downwards. The UI then presents this altered state to the user.

State Hoisting within Jetpack Compose implies elevating the state of the composable outside its boundaries. The primary advantage of making the composable stateless is that it reduces the possibility of inconsistent state representation, thereby decreasing potential bugs. It also increases testability, as it decouples state and UI, allowing individual testing of both entities.

To achieve state hoisting, the state variable is replaced by two parameters:

  1. ‘value: T’ — representing the current state to be depicted
  2. ‘onValueChange: (T) -> Unit’ — an event calling for a state change, where ‘T’ is the new state value proposal

The system then uses observable state holders such as StateFlow or LiveData to automatically reflect any changes in state on the UI.

When applying state hoisting, consider the following guidelines:

  1. Hoist the state at least to the minimum common parent level of all composables that utilize the state (read).
  2. Hoist the state to at least the maximum level at which it can be altered (write).
  3. If multiple states change in response to identical events, hoist them to the same level.

These principles can guide you towards more effective and efficient utilization of state hoisting in Jetpack Compose, enhancing the reliability, consistency, and testability of your application.

let’s see an example to better illustrate the concept of Unidirectional Data Flow and State Hoisting in Jetpack Compose.

Consider a simple composable like a TextField. Here’s how you might implement it with local state:

@Composable
fun MyTextField() {
var text by remember { mutableStateOf("") }

TextField(
value = text,
onValueChange = { newText -> text = newText },
label = { Text("Enter something") }
)
}

In the example above, the state is maintained inside the MyTextField composable. This works fine for simple examples, but as the app becomes more complex and this text field's state is needed elsewhere, this setup becomes more difficult to manage. The state is "buried" inside MyTextField, and not accessible outside of it.

This is where State Hoisting comes into play. The state is “hoisted” up to the caller of MyTextField, and the composable becomes stateless, like this:

@Composable
fun MyTextField(text: String, onTextChange: (String) -> Unit) {
TextField(
value = text,
onValueChange = onTextChange,
label = { Text("Enter something") }
)
}

In the example above, the MyTextField composable no longer manages its own state. Instead, it takes the current value and a function to call when the value changes as parameters. This allows the caller to manage the state instead, providing greater flexibility and consistency:

@Composable
fun MyScreen() {
var text by remember { mutableStateOf("") }

MyTextField(
text = text,
onTextChange = { newText -> text = newText }
)
}

In the MyScreen composable, the state is hoisted to the top level, and is passed down to MyTextField. This way, any changes to the text state are immediately reflected in the MyTextField UI, aligning perfectly with the Unidirectional Data Flow principle. It also makes the state easily accessible and controllable from other composables or ViewModels.

In conclusion, the goal of UDF and state hoisting in Jetpack Compose is to make your composables easier to use, reuse, test, and to help prevent bugs by having a single source of truth for your state.

--

--