State Holders in Jetpack Compose
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
andrememberSavable
- 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 usingremember
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?
- Previous: Turbine and the combine operator
© Donovan J. LaDuke - 2023 | All Rights Reserved
Support - Buy me a Coffee on Ko-fi
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK