I need some help understanding and minimizing reco...
# compose
d
I need some help understanding and minimizing recomposition. Please see the thread.
Copy code
@Composable
fun AppScreenRoot(
    viewModel: AppViewModel = koinViewModel<AppViewModel>(),
    onNotLoggedInToEsm: () -> Unit,
    onNotLoggedInToSms: () -> Unit,
) {
    val state by viewModel.state.collectAsStateWithLifecycle()
        AppScreen(
            state = state,
            onAction = { action ->
                viewModel.onAction(action)
            },
        )
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppScreen(
    state: AppState,
    onAction: (AppAction) -> Unit,
    ) {
    val pagerState = rememberPagerState(
        pageCount = { AppTabs.entries.size },
    )
    val refreshState = rememberPullToRefreshState()

    Column {
        AppTopBar(
            modifier = Modifier.fillMaxWidth().statusBarsPadding().padding(vertical = 10.dp, horizontal = 4.dp),
            pagerState = pagerState
        )

        RefreshIndicator(
            modifier = Modifier.fillMaxWidth(),
            refreshState= refreshState,
            isRefreshing = { state.isRefreshing }
        )

        HorizontalPager(
            modifier = Modifier.fillMaxSize(),
            pageSpacing = 30.dp,
            state = pagerState,
        ) {
            TimetableScreenRoot(
                    onRefreshStart = { onAction(AppAction.onRefreshStart) },
                    onRefreshStop = { onAction(AppAction.onRefreshStop) },
                    refreshState
                )
        }
    }
}
In this code here, everytime state.isRefreshing changes, both TopAppBar and HorizontalPager recompose even though they do not read that isRefreshing state
From my understanding of reading the recomposition docs, the components that do not depend on state that has changed will not recompose, but that doesn't seem to be the case here.
z
What is AppState?
d
Copy code
data class AppState(
    val isEsmLoggedIn: Boolean = false,
    val isSmsLoggedIn: Boolean = false,
    val isInitialized: Boolean = false,
    val isRefreshing: Boolean = false,
)
z
How are you determining that TAB and HP recompose?
d
i have a print statement in TAB
z
Sorry TAB == ATB, misread
Can you post AppTopBar?
d
i have some more experimenting and it looks like if I pass state in a lambda it does not recompose ATB:
Copy code
fun AppScreen(
    state: () -> AppState,
    onAction: (AppAction) -> Unit,
    ) {
But is that really the correct way
Yeah I mean AppTopBar
z
If PagerState is stable and ATB is not inline and returns Unit then it should be skipped when the caller recomposes.
d
Copy code
@Composable
fun AppTopBar(
    modifier: Modifier = Modifier,
    pagerState: PagerState
) {
pagerState is just rememberPagerState, sorry if this is a noob question, but how would I check if it's stable?
z
Well it sounds like there are two separate recompositions you’re trying to figure out. 1. AppScreen recomposes when you pass in a new AppState. That’s expected, if you change isRefreshing you’re creating a new AppState value, and the only way for that to get into AppScreen is to recompose it. You’re not actually using AppState from AppScreen’s composition though, so passing the lambda allows you to avoid recomposing it although it makes the api more complicated and so I would probably keep it a normal parameter unless it’s changing on every frame, like during an animation or scroll. 2. The “children” of AppScreen, ATB and HP. These shouldn’t be recomposing when AppState changes as far as I can see (given assumptions mentioned in previous message). So I’m not sure what’s going on there. Could be that unrelated state just happens to be changing in the same frame.
You can use compose compiler metrics to be absolutely sure, but if the type has the
@Stable
annotation then that also tells you
If it’s this one then it’s stable
I think the latest versions of Android studio have some tooling that can help figure out what caused the recomposition
d
Ok so my assuptions were correct. Could be something else, I'll look into it with the Android studio tooling then, thank you. Pagerstate is that one you linked.
It's the
statusBarPadding
modifier that is causing it to recompose:
Copy code
AppTopBar(
            modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp, horizontal = 4.dp).statusBarsPadding(),
            pagerState = pagerState
        )
If I move it directly into
AppTopBar
then it doesn't recompose but I'd prefer to keep it on the parent screen
z
Hm, that uses
windowInsetsPadding
which is inline AND uses
composed
, i think it might be recomposing because
statusBarsPadding
which calls it passes a no-inline
inspectorInfo
lambda but is not itself composable, which means compose won’t memoize the lambda, so it’s going to be a different instance each time you call the modifier. I bet this is an oversight, i would file a bug.
interesting find!