Steffen Funke
11/05/2021, 7:23 AMLazyColumn
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🧵Steffen Funke
11/05/2021, 7:24 AM// In Fragment, this is called from outside via interface.
// Already plumbed and working, not scope of the question.
override fun onTabReselected() {
scrollableListStateViewModel.scrollToTop()
}
// 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++
}
}
// 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)
) {
...
}
Steffen Funke
11/05/2021, 7:27 AMonTabReselected
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.Steffen Funke
11/05/2021, 7:28 AMvar scrollToTop by mutableStateOf(0)
directly in the Fragment? Or would that introduce leaks because of some observation, that might not be cancelled on destruction?Albert Chang
11/05/2021, 7:47 AM// 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)
}
}
Steffen Funke
11/05/2021, 7:50 AMMutableStateFlow
instead of a mutableStateOf
? I get it, but what is the difference? Both are observable from Compose land.Albert Chang
11/05/2021, 7:50 AMYou shouldn’t use a state to represent events.
Steffen Funke
11/05/2021, 7:51 AMUnit
?Albert Chang
11/05/2021, 7:52 AMMutableStateFlow
, which is still a state, but MutableSharedFlow
without replay.Steffen Funke
11/05/2021, 7:53 AMSteffen Funke
11/05/2021, 7:54 AMSteffen Funke
11/05/2021, 8:11 AMLaunchedEffect
- not sure if this was even necessary.
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 🤔Albert Chang
11/05/2021, 8:14 AMSteffen Funke
11/05/2021, 8:28 AM@Composable
fun Modifier.observeScrollToTopEvents(
scrollToTopEvents: SharedFlow<Unit>?,
listState: LazyListState,
): Modifier {
scrollToTopEvents?.let {
LaunchedEffect(scrollToTopEvents) {
scrollToTopEvents.onEach {
listState.animateScrollToItem(0)
}.launchIn(this)
}
}
return Modifier
}
Usage:
val listState: LazyListState = rememberLazyListState()
LazyColumn(
modifier = modifier
.fillMaxWidth()
.observeScrollToTopEvents(
scrollToTopEvents = scrollToTopEvents,
listState = listState
),
state = listState
) {