Afterglow
Afterglow

Reputation: 405

StateFlow And LazyColumn recomposition

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 { ... }

Upvotes: 2

Views: 567

Answers (2)

Afterglow
Afterglow

Reputation: 405

After a lot of hard exploration, I solved the problem!!! :)

A properly working flow looks like this:

private val activeAccountFlow = accountDao
  .getActiveAccountFlow()
  .distinctUntilChanged { old, new -> old?.id == new?.id }
  .filterNotNull()

val homeCombinedFlow = activeAccountFlow
  .flatMapLatest { account ->
    val timelineFlow = timelineDao.getStatusListWithFlow(account.id)
    timelineFlow.map {
      HomeUserData(
        activeAccount = account,
        timeline = splitReorderStatus(it).toUiData().toImmutableList(),
        position = TimelinePosition(account.firstVisibleItemIndex, account.offset)
      )
    }
  }
  .stateIn(
    scope = viewModelScope,
    started = SharingStarted.Eagerly,
    initialValue = null
  )

Next is the UI part, since only need to recomposition the LazyColumn when the activeAccount is changed, the remember key should be set to activeAccount.id

  val lazyState = rememberSaveable(activeAccount.id, saver = LazyListState.Saver) {
    LazyListState(timelinePosition.index, timelinePosition.offset)
  }
  val firstVisibleIndex by remember(lazyState) {
    derivedStateOf {
      lazyState.firstVisibleItemIndex
    }
  }

Upvotes: 1

Jan Itor
Jan Itor

Reputation: 4256

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)
    }

Upvotes: 2

Related Questions