Given a Fragment with a `LazyColumn` inside a `Com...
# compose
s
Given a Fragment with a
LazyColumn
inside a
ComposeView
, both wrapped in a View based
BottomNavigation
container hierarchy. Goal: I need to pass outside View based events, e.g.
onTabItemReselected
, from containing
BottomNavigationView
to the compose hierarchy, to let my
LazyList
scroll up, as the current tab is reselected. My cumbersome, but working approach now looks like this, I am wondering if there are better ways, to avoid those ViewModel round turns, only to dispatch the
scrollToTop
- Event Thanks - 👉 Code in thread🧵
Copy code
// In Fragment, this is called from outside via interface.
// Already plumbed and working, not scope of the question.

override fun onTabReselected() {
    scrollableListStateViewModel.scrollToTop()
}
Copy code
// Narrow scoped ScrollableListStateViewModel:

class ScrollableListStateViewModel : ViewModel() {

    // using a incrementing counter as "event", to avoid conflation
    var scrollToTop by mutableStateOf(0)
        private set

    fun scrollToTop() {
        scrollToTop++
    }

}
Copy code
// In Fragment, in Composeview, possibly deep in a hierarchy:
...
val listState = rememberLazyListState()

LaunchedEffect(key1 = scrollableListStateViewModel.scrollToTop) {
    listState.animateScrollToItem(0)
}
LazyColumn (
    state = listState,
    modifier = Modifier.align(Alignment.TopStart)
) {
...
}
TLDR: As
onTabReselected
is called on the Fragment from outside, the ViewModel
scrollToTop()
function is called, which increases a counter (to avoid conflation), which in turn triggers a
listState.animateScrollToItem(0)
inside the
LaunchedEffect
near the
LazyColumn
My gripe with this is, that I need to pass down the
scrollableListStateViewModel.scrollToTop
possibly deep down the hierarchy, depending where my list sits.
Could I skip the ViewModel, and, for this special case, store the
Copy code
var scrollToTop by mutableStateOf(0)
directly in the Fragment? Or would that introduce leaks because of some observation, that might not be cancelled on destruction?
a
You shouldn’t use a state to represent events. You should do something like this:
Copy code
// Fragment
private val scrollToTopEvents = MutableSharedFlow<Unit>(
    extraBufferCapacity = 1,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

override fun onTabReselected() {
    scrollToTopEvents.tryEmit(Unit)
}

// Compose
LaunchedEffect(scrollToTopEvents, listState) {
    scrollToTopEvents.collect {
        listState.animateScrollToItem(0)
    }
}
jetpack compose 1
s
Edited my code, since I refactored a bit. So you suggesting storing it inside the Fragment, and just use a
MutableStateFlow
instead of a
mutableStateOf
? I get it, but what is the difference? Both are observable from Compose land.
a
The point is
You shouldn’t use a state to represent events.
s
Got it. And your solution would not conflate multiple emissions of
Unit
?
a
Also note that I’m not using
MutableStateFlow
, which is still a state, but
MutableSharedFlow
without replay.
👍 1
s
But I see it. It is an event, not a state. Thanks. I’ll give it a try
@Albert Chang Thanks for taking your time! Appreciated.
👍 1
It works so far, only adjusted the collection to make explicit use of the CoroutineScope provided by
LaunchedEffect
- not sure if this was even necessary.
Copy code
val lazyListState = rememberLazyListState()

LaunchedEffect(scrollToTopEvents) {
    scrollToTopEvents.onEach {
        lazyListState.animateScrollToItem(0)
    }.launchIn(this)
}
I am just having a hard time understanding why using scrollToTopEvents as a key for
LaunchedEffect
works - since I assume this object would always be the same,
equals
-wise 🤔
a
No it’s not necessary. As for the key, basically you should specify all the captured variables as keys for correctness. The behavior may be the same but it’s always better to be explicit.
🙏 1
s
Created a Modifier for it, I am beginning to like it:
Copy code
@Composable
fun Modifier.observeScrollToTopEvents(
    scrollToTopEvents: SharedFlow<Unit>?,
    listState: LazyListState,
): Modifier {

    scrollToTopEvents?.let {
        LaunchedEffect(scrollToTopEvents) {
            scrollToTopEvents.onEach {
                listState.animateScrollToItem(0)
            }.launchIn(this)
        }
    }

    return Modifier
}
Usage:
Copy code
val listState: LazyListState = rememberLazyListState()

LazyColumn(
        modifier = modifier
            .fillMaxWidth()
            .observeScrollToTopEvents(
                scrollToTopEvents = scrollToTopEvents,
                listState = listState
            ),
        state = listState
    ) {
jetpack compose 1