Skip to main content
Donovan LaDuke - Developer

State Holders in Jetpack Compose


Featured in Android Weekly Issue 583

In a Compose based Android app it is a common scenario to find yourself managing state coming from both an app state container like a viewmodel and ui specific state from compose, which can quickly become an unreadable mess. What if we introduced a pattern to clarify and streamline the state so that we can treat it as a single stream of data?

An Average Compose Screen #

Here is a pretty common screen structure you will see in a Compose based UI on Android.

@Composable
fun ExampleScreen(vm: ExampleViewModel = viewModel()) {
  val state: List<String> by vm.state.collectAsState()
  val scope = rememberCoroutineScope()
  val lazyListState = rememberLazyListState()
  val scrollToBottom = remember<() -> Unit> {
    {
      scope.launch {
        lazyListState.scrollToItem(state.lastIndex)
      }
    }
  }

  ExampleScreen(state, lazyListState, scrollToBottom)
}

In the screen composable we collect the state from the viewmodel, remember a coroutine scope and lazy list state, and finally combine those together to create a scrollToBottom function before passing all of it to another composable to be displayed.

This component is doing a lot! It's taking state from multiple sources, bringing them together, mapping them to secondary state (like the scrollToBottom function), and then sending those pieces off separately to be rendered. What if we could abstract away the state coordination logic so our screen could focus solely on UI?

Introducing a State Holder #

Let's introduce a new data class to represent our screen state and a composable function to aggregate all that data.

data class ScreenState(
  val content: List<String>,
  val lazyListState: LazyListState,
  val scrollToBottom: () -> Unit
)

@Composable
fun rememberScreenState(
  state: List<String>,
  lazyListState: LazyListState = rememberLazyListState(),
  scope: CoroutineScope = rememberCoroutineScope(),
): ScreenState {
  val scrollToBottom = remember<() -> Unit> {
    {
      scope.launch {
        lazyListState.scrollToItem(state.lastIndex)
      }
    }
  }

  return ScreenState(
    content = state, 
    lazyListState = lazyListState, 
    scrollToBottom = scrollToBottom,
  )
}

@Composable
fun ExampleScreen(vm: ExampleViewModel = viewModel()) {
  val state by vm.state.collectAsState()

  val screenState = rememberScreenState(state = state)

  ExampleScreen(screenState)
}

We now have a single class, ScreenState, that represents the state of our entire screen. In our rememberScreenState function, we handle all the compose screen state and viewmodel state and combine them into that class. Now all our downstream ExampleScreen composable has to handle is converting the ScreenState data class into UI. No more combining our display and state coordination logic!

Want to see a more complex example? Check out this project on my GitHub page!

Additional Tips #

Conclusion #

State holders can be a new tool in you tool box when building Andriod apps using Jetpack Compose. I hope you've seen how they can clarify and streamline your state management to create a more readable and testable UI. Until next time, thanks!

Did you find this content helpful?

Please share this post and be sure to subscribe to the RSS feed to be notified of all future articles!

Want to go above and beyond? Help me out by sending me $1 on Ko-fi. It goes a long way in helping run this site and keeping it advertisement free. Thank you in advance!

Buy me a Coffee on Ko-fi