I have a question related to StateFlow
and UI recomposition. In short, my ViewModel has three flows:
accountFlow
, which is used to fetch the currently logged-in account from the database.
timelinePositionFlow
, which is used to retrieve the last browsing position record from the currently logged-in account.
timelineFlow
, which contains all posts stored in the database for the currently logged-in account.
The issue I'm facing is that my app can switch between multiple accounts and browse information. Since each account has a stored timeline position, I need to use rememberLazyListState(initialFirstVisibleItemIndex = ...)
to initialize the position of the LazyColumn. However, if I switch accounts and StateFlow emits the value of the new account, the rememberLazyListState
won't recomposition. so that the size of the timeline of the new account may only be 20, and the timeline Position Index of the old account is greater than 20, which will cause a java.lang.IndexOutOfBoundsException
error
Is there a good way to solve this problem?
viewModel
private val activeAccountFlow = accountDao
.getActiveAccountFlow()
.filterNotNull()
.distinctUntilChanged { old, new -> old.id == new.id }
val timelinePosition = activeAccountFlow
.mapLatest { TimelinePosition(it.firstVisibleItemIndex, it.offset) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = TimelinePosition()
)
val timeline = activeAccountFlow
.flatMapLatest { timelineDao.getStatusListWithFlow(it.id) }
.map { splitReorderStatus(it).toUiData().toImmutableList() }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = persistentListOf()
)
UI
val timeline by viewModel.timeline.collectAsStateWithLifecycle()
val timelinePosition by viewModel.timelinePosition.collectAsStateWithLifecycle()
val lazyState = rememberLazyListState(
initialFirstVisibleItemIndex = timelinePosition.index,
initialFirstVisibleItemScrollOffset = timelinePosition.offset
)
LazyColumn { ... }
Try saving LazyListState
manually. By setting the first argument of rememberSaveable
to timelinePosition
, lazyState
will reset when timelinePosition
changes.
val lazyState = rememberSaveable(timelinePosition, saver = LazyListState.Saver) {
LazyListState(timelinePosition.index, timelinePosition.offset)
}
Edit:
If timelinePosition
is updated on lazyState
change, then this approach won't work, because it will introduce a cyclic dependency. In this case i would recommend to change the way you are getting timelinePosition
. You don't really need a StateFlow
, as you only need to read it once when the active user changes. Add a one-shot suspend method to the accountDao
and ViewModel
to get timelinePosition
and use it when timeline
changes, something like:
LaunchedEffect(timeline) {
timelinePosition = viewModel.getTimeLinePosition(activeUserId)
lazyState.animateScrollToItem(timelinePosition.index, timelinePosition.offset)
}