4

I am looking for an efficient way to trigger a callback for each item of a LazyColumn as they become visible, but only once.

  • The callback should happen only once as items become visible. It should not trigger if the user scrolls past the same item several times.
  • The callback should only happen once per each item.

Is there a way Compose-y way of handling this?

I tried to use snapshotFlow as below, but no matter which side effect I use, it gets triggered over and over as a user scrolls.

val listState = rememberLazyListState()
LaunchedEffect(listState) {
    snapshotFlow { listState.layoutInfo.visibleItemsInfo}
        .map { it.first() }
        .collect {
            MyAnalyticsService.someVisibleItemCallback()
        }
}

Another way I can image is baking this into the model state as follows.

data class SomeObject(
  val someStuff: SomeStuff,
  val isSeen: Boolean = false
)

How can I handle this in an efficient way?

Sudhir Singh Khanger
  • 1,598
  • 2
  • 17
  • 34

2 Answers2

0

Just change your code to :

 snapshotFlow { listState.layoutInfo.visibleItemsInfo}
    .map { it.first() }
    .distinctUntilChanged()
    .collect {
        MyAnalyticsService.someVisibleItemCallback()
    }

Distinct until changed will prevent your flow from being called until your value changes

Arsh
  • 279
  • 2
  • 6
  • 1
    But for a large list being scrolled the component will get mounted and unmounted, won't it. In such a case won't `LaunchedEffect` be called again? And won't it be a new `snapshotFlow`? – clamentjohn Oct 21 '22 at 04:32
  • Yes it will be called again but you change your visibility value will be changed only once. Store visibility for all the items in your VM and simply change it for the first and last time when the item comes in view. The distinctUntilChanged will not execute again and again – Arsh Oct 21 '22 at 14:01
  • 1
    If I store visibility as a Boolean in the data class then what role does snapshotFlow plays as you mentioned in the answer. If I have it saved in the model then I would just check for it and if it is false then fire the LaunchedEffect. – Sudhir Singh Khanger Oct 22 '22 at 04:36
  • I am trying to think if `distinctUntilChanged()` would be useful in any case. I don't see a case where list item remains the same in which case flow should not emit? – Sudhir Singh Khanger Oct 22 '22 at 04:43
0

When a LazyColumn item is "recycled", the item will be re-initialized including side-effects it has.

I had a similar requirement and attempted to utilize rememberUpdatedState, sadly to no avail, it didn't satisfy what I wanted because what ever I do, LazyColumn's item keeps being recycled, so I just ended up adding an additional attribute to my data class, something that would "persist" outside of the recycling like your isSeen boolean property.

isInitialized: Boolean

Making sure this flag wraps my callback.

@Composable
fun ListItemComposable(
    item: Item,
    doneInitCallback : (Item) -> Unit
) {
    LaunchedEffect(Unit) {
        if (!item.isInitialized) {
            doneInitCallback(item)
        }
    }
    ....
}

If there are other ways, I'm not sure, though the closest solution we can find is using either rememberUpdatedState, your attempt to use snapShotFlow or rememberSaveable, but again every item is being recycled as you scroll. I haven't tried using rememberSaveable yet for this situation though.

Also have a look at Phil Dukhov's answer.

z.g.y
  • 5,512
  • 4
  • 10
  • 36
  • Why is item.isInitialized check inside the side effect? Should it not be outside? Or maybe I am wrong at least when you are in the view you want to avoid the check on recomposition. Any other problems have you experienced with this approach? Any slowdowns? – Sudhir Singh Khanger Oct 22 '22 at 04:40
  • I consider this kind of scenario as a "Side-Effect" that's why I wrap it inside `LaunchEffect` . The only issue I have with this approach is the item will execute a second re-composition as soon as you set the boolean flag to true, but the `snapshotFlow` approach looks promising, though I haven't been able to check it yet if it can circumvent the secondary re-composition issue – z.g.y Oct 22 '22 at 06:05
  • Also thank you if you voted it up, I was waiting for some correction or recommendation from the one who voted it down as I find this something hard to work around with. If I may ask, is the size of your list fixed? or undetermined?, because I have another situation where I just ended up using a `Scrollable Column`, knowing that it's a fixed size didnt cause me any problems. – z.g.y Oct 22 '22 at 06:10
  • 1
    1. I am not seeing any benefit of snapshotFlow because the LaunchedEffect will execute anyway. So will snapshotFlow. – Sudhir Singh Khanger Oct 22 '22 at 10:01
  • 1
    Size depends on API response but except more or less 100 items. But downside of Column is that 100 columns will be created and there is recycling of them. – Sudhir Singh Khanger Oct 22 '22 at 10:02
  • @SudhirSinghKhanger, Column won't recycle, all items are eagerly loaded such in a loop-construct so it won't do any recycling, that's why a one time callback worked for me in a different situation I mentioned, not sure why yours is recycling though.. – z.g.y Oct 22 '22 at 15:52
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/248984/discussion-between-sudhir-singh-khanger-and-z-y). – Sudhir Singh Khanger Oct 22 '22 at 16:07