Hi folks, I have a question related to `StateFlow`...
# compose
n
Hi folks, I have a question related to
StateFlow
and
UI recomposition
. In short, my
ViewModel
has three flows: 1.
accountFlow
, which is used to fetch the currently logged-in account from the database. 2.
timelinePositionFlow
, which is used to retrieve the last browsing position record from the currently logged-in account. 3.
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
Copy code
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
Copy code
val timeline by viewModel.timeline.collectAsStateWithLifecycle()
  val timelinePosition by viewModel.timelinePosition.collectAsStateWithLifecycle()

  val lazyState = rememberLazyListState(
    initialFirstVisibleItemIndex = timelinePosition.index,
    initialFirstVisibleItemScrollOffset = timelinePosition.offset
  )

  LazyColumn { ... }
a
You can just put
rememberLazyListState
inside a
key(account) { }
.
🤔 1
d
.combine()
could help too - put the 2/3 states into 1 and filter out these half-updates that are causing you problems
c
What version of compose are you using?
n
1.6.0-alpha8
c
I think this is a crash from alpha08
n
I tested beta version, but it happened again, and 1.6.0 beta has some LazyColumn issues 🤔
c
Damn. Yeah, I just moved to alpha08 and released to prod and now I have hundreds of these crash reports in crashlytics. lol
You should file a bug if you have a reproducible sample. I can't repro.
n
Actually i submitted other LazyColumn issue to issueTacker, but I’m not sure if this problem is a about LazyColumn https://issuetracker.google.com/issues/312033389
v
The
checkIndexBounds
crash in alpha08 is most likely this: https://issuetracker.google.com/issues/295745063#comment57
n
I tried using `Albert`'s method and also tried using remember(timelinePosition) { LazyListState(...) } to manually recomposition, but it doesn't seem to solve the problem. Firstly, the firstVisibleIndex in LazyState does not update. However, I have some functionalities that rely on listening to
lazystate.firstVisibileIndex
, such as saving the position of the timeline
c
Are you using compose stable?
n
no, still alpha
c
try stable?
n
@dorche but my timelineFlow requires an active account id, how do I combine it with accountFlow?
a
the firstVisibleIndex in LazyState does not update.
I would assume there's some problem with your code, or maybe your timeline becomes empty temporarily.
However, I have some functionalities that rely on listening to
lazystate.firstVisibileIndex
, such as saving the position of the timeline
Just add lazy list state as a key.
You might also want to do some debugging to make sure the three values are updated in the same frame. If you are updating the flows separately, and/or using some flow transformations which are not synchronous, they can easily fall out of sync.
👍 1
d
The
activeAccountFlow
is already private, you should be able to do make
timelinePosition
and
timeline
flows private too, and only expose one
screenState
(for example) StateFlow to Compose to consume, which then should have all the data in a good state, without mismatches.
n
update: I seem to be getting close to solving this problem, but, I ran into some minor issues, detail: https://kotlinlang.slack.com/archives/CJLTWPH7S/p1700892289386529
i created a minimal project to test it, if anyone is interested, you can give it a try , repo: https://github.com/whitescent/multi-account-flow-test
a
This looks exactly like the out of sync problem I suggested.
```val timeline = activeAccountFlow
.flatMapLatest { timelineDao.getStatusListWithFlow(it.id) }```
This introduces some asynchronism and the problem is likely because timeline is updated after the other two (so the UI will first be composed with the old value of timeline and the new values of the other two, and then be composed with all the new values).
n
thank you @Albert Chang I just solved this problem ! now, this flow looks like:
Copy code
private val activeAccountFlow = accountDao
    .getActiveAccountFlow()
    .filterNotNull()
    .distinctUntilChanged { old, new -> old.id == new.id }

  val combinedFlow = activeAccountFlow
    .flatMapLatest { activeAccount ->
      val timelineFlow = timelineDao.getStatusListWithFlow(activeAccount.id)
      timelineFlow.map {
        UiState(activeAccount.id, it, TimelinePosition(activeAccount.index, activeAccount.offset))
      }
    }
    .stateIn(
      scope = viewModelScope,
      started = SharingStarted.WhileSubscribed(),
      initialValue = null
    )
I didn't think it would work out that way 😅
c
albert is the GOAT