Search code examples
androidkotlinandroid-jetpack-composekotlin-coroutines

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

Solution

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