1

State Holders in Jetpack Compose

 9 months ago
source link: https://dladukedev.com/articles/006_stateholders/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

State Holders in Jetpack ComposeSkip to main content

State Holders in Jetpack Compose

August 10, 2023deep dive android development compose

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 #

  • Don't preemptively introduce a state holder if you don't need it - Our contrived example got entirely more complicated by introducing a state holder! Only introduce a state holder when it will help clarify and simplify your composable. My rough rule of thumb is when there are more than 3 effects or remember blocks, I'll think about introducing a state holder.
  • Prevent unnecessary rerenders using remember and rememberSavable - Due to aggregating all our state into a single class, it becomes more important that we aren't creating new instances of that class unnecessarily. We can do that using remember blocks in our state holder.
  • State holders built in a composable will follow the composable lifecycle - Unlike a viewmodel, our state holder is NOT lifecycle aware. If you need any parts of the state to persist across configuration changes, consider using rememberSavable or moving that state into your viewmodel.
  • Don't pass complex classes into your state holder - Keep your state holder simple and testable by passing parts of a complex class like a viewmodel instead of passing in the entire class.
  • State holders introduce new testing opportunities - By grouping our state into a single class that is the result of a function, we can test the output in a functional matter. You could even test the screen "headless" if you wanted to.

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?

© Donovan J. LaDuke - 2023 | All Rights Reserved

RSS feed - XML | JSON

Support - Buy me a Coffee on Ko-fi


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK