0

I have a data class which represents the state for my screen:

@Parcelize data class State( @IgnoredOnParcel val list: List<ViewType> = emptyList(), val isLoading: Boolean = false, val isDialogFilterDisplayed: Boolean = false, val filter: Int? = null, val order: String = ORDER_ASC, val year: String = "", val page: Int = 1, val scrollPosition: Int = 0 ): Parcelable 

My composable looks like:

@OptIn(ExperimentalMaterialApi::class) @ExperimentalCoroutinesApi @Composable internal fun Route( modifier: Modifier = Modifier, viewModel: ViewModel, refreshState: PullRefreshState, onClicked: (links: Links) -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() Screen( list = uiState.list, isLoading = uiState.isLoading, page = uiState.page, modifier = modifier, onChangeScrollPosition = viewModel::setScrollPositionState, loadNextPage = viewModel::nextPage, pullRefreshState = refreshState, onClicked = onClicked ) } 

My problem is anytime any variable in the data class like the position, page etc get updated in the Viewmodel, it triggers a recomposition of the very long complex LazyVerticalGrid in the Screen composable even though the "list" variable isn't changing. I'm a bit perplexed how to prevent these recompositions.

My only solution is to split the state data class into three separate ones where the uiState data class only contains the list variable and nothing else. Then make a PaginationState and stick the page, position in there then a FilterState and stick the order, year, filter in there, This would mean having three different MutableStateFlows in the Viewmodel and would in theory prevent these recompositions. I'm just not sure what is the industry best practice for my use case. Any thoughts and suggestions from experienced compose folks would be very welcome!

3
  • Make sure ViewType is stable and consider switching to ImmutableList from kotlinx.collections.immutable. Beyond that, there may be things you can do inside Screen() to help. Commented Oct 11, 2023 at 17:50
  • Thanks for the suggestions but I think you misread the question, the composable isnt recomposing because of the list, its the other variables in the data class causing the recomposition. These variables are set in the Viewmodel and not directly in any composables. Commented Oct 11, 2023 at 18:43
  • Having a stable immutable list may help with "a recomposition of the very long complex LazyVerticalGrid in the Screen composable even though the "list" variable isn't changing". The LazyVerticalGrid() thinks that your list is changing when you believe it is not. Commented Oct 11, 2023 at 22:39

1 Answer 1

2

First thing to do decrease count of recompositions is to use Scopes. It doesn't matter if you use one MutableState or your inputs are stable if all of the variables are read in same Composable.

Jetpack Compose Scoped/Smart Recomposition

And do not pass State/MutableState itself to these functions, pass data classes or primitives and use state hoisting here to not pass ViewModel deep down.

Then instead of keeping data in one place turn your state into holder that contains other MutableState or other values such UiState as in JetSnack or other Google samples, or standard remember functions like ScrollState or other rememberXState functions, since you have a ViewModel you don't need a remember function for this UiState.

@Stable class UiState( val list = mutableStateListOf<ViewType>() var isLoading by mutableStateOf(false) var isDialogFilterDisplayed by mutableStateOf(false) ) 

Inside ViewModel in collect set list or other variables and after user interaction you can set any of these state variables and pass values to Composables not as UiState but as Boolean, Int or callbacks.

@OptIn(ExperimentalMaterialApi::class) @ExperimentalCoroutinesApi @Composable internal fun Route( modifier: Modifier = Modifier, viewModel: ViewModel, refreshState: PullRefreshState, onClicked: (links: Links) -> Unit ) { val uiState by viewModel.uiState Screen( list = uiState.list, isLoading = uiState.isLoading, page = uiState.page, modifier = modifier, // these callbacks are likely to be unstable, you can wrap them with remember onChangeScrollPosition = remember { viewModel.onChangeScrollPosition()}, loadNextPage = remember { viewModel.nextPage }, pullRefreshState = refreshState, onClicked = onClicked ) } 

Actually instead of the you can set page as this, by creating scopes and passing values of UiState these scopes will only recomposed when their inputs change.

 @Composable private fun MyScreen( viewModel:ViewModel, refreshState: PullRefreshState, onClicked: (links: Links) -> Unit ) { val uiState = viewModel.uiState Header(uiState.SomeInput, callback) ItemList(uiState.list) BottomNavigation(uiState.AnotherInput) } 

As last step, if you still have performance issues check out stability issues with function params and callbacks.

https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8

Also ViewModel itself being unstable, calls to its functions might also be unstable as well.

Why does a composable recompose while seemingly being stateless (the only passed parameter is a function, not a state)

You can also refer Stability Edit of this answer.

And if you are passing a value that changes in every frame you can consider deferring reads to layout or draw phases, this might be helpful in case of animations.

Basically if a value changes often pass it inside a lambda and use Modifiers with lambda Modifier.offset{} over Modifier.offset() for instance.

https://stackoverflow.com/a/73681007/5457853

Sign up to request clarification or add additional context in comments.

3 Comments

Also for ViewModel callbacks you can refer this answer stackoverflow.com/q/76963088/5457853
Thank you for your advice im currently reviewing your answer and the associated link you posted. The first question that jumps out at me is that I notice your using MutableState I was always under the impression that state must always be immutable, can you explain your choice for using mutable state over immutable?
It's delegation there. It's same as val state = MutableState() state.value = somevalue.cs.android.com/androidx/platform/frameworks/support/+/…

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.