Jetpack Compose is a modern UI toolkit for building Android applications. One of its key concepts is state management, which allows you to handle and update data in a reactive manner. In this article, we will explore the basics of state management in Jetpack Compose and discuss the important question of where to hoist state in your Compose UI hierarchy.
Types of states
When we talk about UI State, we come across two different definitions:
Screen UI State: This is basically what you need to display on the screen. Let’s say you have a list that you obtain data from an API and show to the user. When the lists change, the UI also needs to be updated. This logic is not relevant to UI components. We refer to this kind of logic as the screen UI state. It is typically connected with other layers of the hierarchy because it contains app data.
UI element state: The UI element state is managed externally to the composable and can be moved to the calling composable function or a state holder. Unlike Android Views, where the state is managed internally, Jetpack Compose treats state as separate from the composable.
Let’s examine each one in depth;
Screen UI State
In Jetpack Compose, the screen UI state is determined by applying business rules. To manage this state, it is common practice to hoist the screen UI state to a screen-level state holder, often implemented as a ViewModel. By hoisting the state to the ViewModel, it allows for centralized management and coordination of the screen’s UI state, ensuring consistency and separation of concerns in the application architecture.
As seen in the example above, we have handled all the situations that may occur on the screen in the ViewModel class. This state is connected with other layers of the hierarchy and contains app data to be displayed to the user.
We used Sealed Interface for screen state. However, we can also use the data class for screen states as below:
While these two approaches serve the same purpose, however, the logic of handling the state in composables is different.
While using the data class, we can overlap UI components; when we use the sealed interface, we can show UI components belonging to only one state.
UI Element State
The UI element state refers to characteristics of UI elements that affect how they look and behave. For example, a UI element can be shown or hidden, have a specific font, font size, or font color. Also, the state is stored separately from the UI element itself. It can be placed in the surrounding code or a designated state holder. This allows for more flexibility and control over how the UI elements behave and appear.
Let’s examine with an example;
In the above example, we keep the component visibility state and the animation state inside the composable method.
What is State Hoisting?
State hoisting in Jetpack Compose is a pattern where the state of a composable is moved to its caller, making the composable itself stateless. This is achieved by replacing the state variable with two parameters: the current value to display and an event handler that triggers a change in the value. By hoisting the state, we ensure a single source of truth, the encapsulation of state modification within stateful composables, the ability to share state across multiple composables, the interception and modification of events by callers, and the decoupling of the state from the composable itself.
In the official example case, you extract the name and the onValueChange out of HelloContent and move them up the tree to a HelloScreen composable that calls HelloContent
State hoisting simplifies the understanding, reusability, and testing of the HelloContent composable. By moving the state out of HelloContent, it becomes independent of how the state is stored. This decoupling ensures that modifications or replacements of HelloScreen don’t require changes to the implementation of HelloContent.
Lastly, we will talk about state hoisting key points before finishing the article:
Key Point: When hoisting state, there are three rules to help you figure out where the state should go:
- The state should be hoisted to at least the lowest common parent of all composables that use the state (read):
This means that the state should be moved to a level in the composable hierarchy where all the composables that need to access or read the state can reach it. By hoisting the state to this common parent, it ensures that all relevant composables have access to the state.
2. The state should be hoisted to at least the highest level at which it may be changed (write):
This rule suggests that the state should be moved to a level where it can be modified or changed by the relevant composable. By hoisting the state to this highest level, it ensures that the necessary composables have the ability to update the state when needed.
3. If two states change in response to the same events, they should be hoisted together:
If multiple states are modified or updated in response to the same events or actions, it is recommended to hoist them together. By hoisting them together, it maintains the relationship between these states and ensures that they are managed consistently when changes occur.
To summarize, it’s worth noting that while you can hoist state higher than these rules require, underhoisting state (not hoisting it to the appropriate level) can make it challenging, or even impossible to follow the desired unidirectional flow of data between composables. Therefore, it is important to carefully consider these rules while hoisting state to maintain a clear and manageable data flow in your application.