Unlock the Power of State Hoisting in Jetpack Compose

Teni Gada
6 min readJun 1, 2024

--

Jetpack Compose is a modern toolkit for building native Android UI. It simplifies UI development on Android by offering less code, powerful tools, and intuitive Kotlin APIs. One of the key concepts in Jetpack Compose is state management, and a crucial aspect of this is state hoisting.

What is State Hoisting?

State hoisting is a pattern in Jetpack Compose that involves moving UI state up the composable hierarchy to the lowest common ancestor (LCA). This common ancestor then manages and controls the state, making it accessible to all its child composables that need it.

This is achieved by creating state in a composable function and passing it down to child composables through function parameters. By passing the state down as function parameters, state hoisting decouples the state from the UI elements, leading to improved code modularity, reusability, testability, and maintainability.

Example of State Hoisting

Let’s dive into an example to see how state hoisting works in practice.

Without Hoisting

@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
  • In this example, the state (count) is local to the Counter composable. This means any composable nested within Counter wouldn’t have access to it.
  • Modifying the count directly within the onClick handler might lead to potential state management issues as the composable recomposes.

Benefits of Hoisting (Coming Up):

We’ll see how hoisting the state (count) in this example improves modularity, reusability, and testability compared to the current approach.

With Hoisting

To hoist the state, follow these steps:

  1. Move the State Up: First, move the count state and the onIncrement logic out of the Counter composable.
  2. Add Parameters: Modify the Counter composable to accept count and onIncrement as parameters.
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Column {
Text("Count: $count")
Button(onClick = onIncrement) {
Text("Increment")
}
}
}

3. Manage State in the Parent: In the parent composable, manage the count state and pass it down to the Counter.

@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
Counter(count = count, onIncrement = { count++ })
}

In this way, Counter becomes a stateless composable that relies on its parent for the state, while CounterScreen handles the state management. By hoisting the state, Counter becomes more reusable as it doesn’t rely on internal state management.

Benefits of State Hoisting

  • Maintainability: Code becomes easier to understand and modify when state is close to where it’s used.
  • Testability: Isolating state makes it easier to write unit tests for your composables.
  • Reusability: Composable functions become more reusable when they don’t rely on state defined elsewhere.

For example, a composable for a counter can be reused across different screens if the counter logic is hoisted to a parent composable that manages the count state. This way, the counter composable itself can be focused on displaying the count and handling user interaction without worrying about managing its own state.

Diving deeper into state hoisting, let’s see how it’s applied in different scenarios

Where to Hoist State?

In a Jetpack Compose application, the decision of where to hoist UI state depends on whether it’s primarily used for UI logic or business logic. This article explores these two main scenarios and the benefits of hoisting state effectively.

Before we dive into hoisting state, let’s understand the some terms UI State, UI Logic and Business Logic:

Understanding UI State, UI Logic and Business Logic:

  • UI State: This refers to the data that controls the visual appearance of the UI elements, such as the text displayed in a button or the selected item in a dropdown menu.
  • UI Logic: This refers to how UI state is presented on the screen. Examples include determining UI element behavior based on user interaction (like showing/hiding a button) or updating visuals based on other UI state changes.
  • Business Logic: This involves implementing core functionalities of your app that handle data manipulation and business rules. An example is saving user preferences or interacting with a database.

Now that we understand the different terms in Jetpack Compose (UI state, UI logic, and business logic), let’s explore how state hoisting works in these two main scenarios:

Scenario 1: State Hoisting for UI Logic

Consider a product filter screen with a category dropdown and a search bar. Here’s how state hoisting can improve UI logic:

  • State: Currently selected category (dropdown selection) and search query (text input)
  • UI Logic: Update search bar hint based on selected category (e.g., “Search shoes” vs. “Search shirts”).Filter displayed products based on search query and selected category.

We would hoist the state (selected category and search query) to the lowest common ancestor (LCA) of the components that require access to this state, which is the ProductFilterScreen.

// ProductFilter.kt
@Composable
fun ProductFilter(
selectedCategory: String,
onCategorySelected: (String) -> Unit,
searchQuery: String,
onSearchQueryChanged: (String) -> Unit
) {
// UI elements for category dropdown and search bar
DropdownMenu(selectedCategory, onCategorySelected)
SearchBar(searchQuery, onSearchQueryChanged)
}


// ProductFilterScreen.kt
@Composable
fun ProductFilterScreen(viewModel: ProductFilterViewModel = viewModel()) {

var selectedCategory by remember { mutableStateOf("") }
var searchQuery by remember { mutableStateOf("") }

ProductFilter(
selectedCategory = selectedCategory,
onCategorySelected = { category -> selectedCategory = category },
searchQuery = searchQuery,
onSearchQueryChanged = { query -> searchQuery = query }
)
}

Scenario 2: State Hoisting for Business Logic:

Let’s say the product filter screen has a “Save Filter” button. Clicking this button saves the current filter settings (category and search query) for future use. Here’s how state hoisting works:

  • State: Selected category and search query (same as UI logic example)
  • Business Logic: Save filter settings to user preferences

While the ProductFilter composable stores the state, it might not be the best place for saving logic (business logic). A ProductFilterViewModel can be used as a state holder.

For saving filter settings, we would hoist the state to a ViewModel, as it’s responsible for managing the application’s data and business logic. The ProductFilter composable would access the state from the ViewModel and trigger a function in the ViewModel to save the filter settings when the button is clicked.

// ProductFilterViewModel.kt
class ProductFilterViewModel : ViewModel() {

private val _selectedCategory = mutableStateOf("")
val selectedCategory: State<String> = _selectedCategory

private val _searchQuery = mutableStateOf("")
val searchQuery: State<String> = _searchQuery

fun setSelectedCategory(category: String) {
_selectedCategory.value = category
}

fun setSearchQuery(query: String) {
_searchQuery.value = query
}

fun saveFilterSettings() {
// Save filter settings logic here
}
}


// ProductFilter.kt
@Composable
fun ProductFilter(viewModel: ProductFilterViewModel) {
// UI elements for category dropdown and search bar
DropdownMenu(
selectedCategory = viewModel.selectedCategory.value,
onCategorySelected = viewModel::setSelectedCategory
)
SearchBar(
searchQuery = viewModel.searchQuery.value,
onSearchQueryChanged = viewModel::setSearchQuery
)

Button(onClick = viewModel::saveFilterSettings) {
Text("Save Filter")
}
}


// ProductFilterScreen.kt
@Composable
fun ProductFilterScreen(viewModel: ProductFilterViewModel = viewModel()) {
ProductFilter(viewModel = viewModel)
}

Choosing the Right State Holder

  • Composables: If the state and logic are simple and only used by that specific composable, keep the state internal.
  • Plain State Holder Classes: For complex state or when multiple composables need to access the state, use a dedicated class. For example, LazyListState manages the scroll position for LazyColumn or LazyRow.
  • ViewModels: Use ViewModels for screen-level state that involves business logic and data preparation for the UI.

While ViewModels are a common choice for hoisting state that involves business logic and data preparation, other solutions like State composables or custom state holder objects might be suitable depending on the specific needs. State composables are a lightweight option for managing simple state that is local to a portion of the composable tree. Custom state holder objects can be useful for complex state or when state needs to be shared between composables that don’t have a common ViewModel ancestor.

By hoisting state effectively, you create a clean separation between UI logic, handled by composables and state holders, and business logic, managed by ViewModels. This leads to well-structured, maintainable, and testable Jetpack Compose applications. Remember, keep your state close to where it’s used, and hoist it up the hierarchy as needed to find the most fitting “home” for it.

Conclusion

State hoisting is a game-changer in Jetpack Compose! It promotes cleaner code separation, improved reusability, and enhanced testability. By understanding the distinction between UI logic and business logic, you can effectively leverage state hoisting to create well-structured and maintainable Jetpack Compose applications.

👏If you found this explanation helpful, give this article a clap!

For a deeper dive into Jetpack Compose’s core principles, including declarative UI, unidirectional data flow, and state management, check out this insightful blog post on Medium: Jetpack Compose: A Powerful Tool for Building Modern Android UIs

Here are some additional resources you might find helpful:

Stay tuned! Get ready to explore the power of advanced Jetpack Compose features in the next article. These concepts will help you build even more dynamic and interactive UIs!

In the meantime, feel free to follow me for more insightful content on Android development.

--

--

Teni Gada

Android Developer | Sharing knowledge & learning from others