3

I am using a LazyColumn and there are several items in which one of item has a LaunchedEffect which needs to be executed only when the view is visible.

On the other hand, it gets executed as soon as the LazyColumn is rendered.

How to check whether the item is visible and only then execute the LaunchedEffect?

LazyColumn() {
    item {Composable1()}
    item {Composable2()}
    item {Composable3()}
.
.
.
.
    item {Composable19()}
    item {Composable20()}

}

Lets assume that Composable19() has a Pager implementation and I want to start auto scrolling once the view is visible by using the LaunchedEffect in this way. The auto scroll is happening even though the view is not visible.

  LaunchedEffect(pagerState.currentPage) {
    //auto scroll logic
  }
Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
Ali_Waris
  • 1,944
  • 2
  • 28
  • 46
  • 1
    Please add a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) – Phil Dukhov Nov 08 '21 at 13:37
  • LaunchedEffect is working exactly as intended. Even though your item may not be visible, it still forms part of the composition because LazyColumns have prefetching. The item's LaunchedEffect will be called in this case. If you want to make sure the item is actually visible on the screen, you may want to check for it's position inside it's parent and use the value from there – Rafsanjani Nov 08 '21 at 14:05
  • @PhilipDukhov updated the question with example – Ali_Waris Nov 08 '21 at 14:27
  • @Rafsanjani How do I check whether the Composable19() is visible or not? using listState? I see it has firstVisibleIndex only. Please provide your inputs. – Ali_Waris Nov 08 '21 at 14:29
  • If `LaunchedEffect` is inside `Composable19` and the other views has non zero size it shouldn't be called until `Composable19` starts appearing – Phil Dukhov Nov 08 '21 at 14:34
  • @PhilipDukhov Yes, the LaunchedEffect is inside Composable19(). Still, the auto scrolling begins once the LazyColumn is rendered. – Ali_Waris Nov 08 '21 at 14:43
  • @Ali_Waris please provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). [Here](https://gist.github.com/PhilipDukhov/c9ed784fea0da0c7dad2339cc068d413) is mine, logs only the visible items when they appears – Phil Dukhov Nov 08 '21 at 15:57
  • Problem is with the pagerState.currentPage as the key of LaunchedEffect, it seems like. – Ali_Waris Nov 08 '21 at 16:11

2 Answers2

2

LazyScrollState has the firstVisibleItemIndex property. The last visible item can be determined by:

val lastIndex: Int? = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index

Then you test to see if the list item index you are interested is within the range. For example if you want your effect to launch when list item 5 becomes visible:

val lastIndex: Int = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1

LaunchedEffect((lazyListState.firstVisibleItemIndex > 5  ) && ( 5 < lastIndex)) {
  Log.i("First visible item", lazyListState.firstVisibleItemIndex.toString())

      // Launch your auto scrolling here...
}

LazyColumn(state = lazyListState) {

}

NOTE: For this to work, DON'T use rememberLazyListState. Instead, create an instance of LazyListState in your viewmodel and pass it to your composable.

Johann
  • 27,536
  • 39
  • 165
  • 279
  • Can I create the instance of LazyListState before executing the Composable19()? I don't have a viewModel in place right now – Ali_Waris Nov 08 '21 at 15:16
  • The LazyListState has nothing to do with the items you are adding to the LazyColumn, as shown in the code above. The LazyListState is assigned to the LazyColumn. So you have to create LazyListState before your composable is even called. You can use the "hoisted state" pattern to pass it into your composable. See https://developer.android.com/jetpack/compose/state#state-hoisting – Johann Nov 08 '21 at 15:27
  • The lastIndex is not giving me correct value :( – Ali_Waris Nov 08 '21 at 16:25
  • What does it return? I tested the code and it does work. – Johann Nov 08 '21 at 16:38
  • Returns visibleItemsInfo with size 2 always. (which seems incorrect in my case) – Ali_Waris Nov 08 '21 at 16:53
  • Are you using a viewmodel to host your LazyListState? Maybe you can post your code showing how you are using LazyListState. And what version of Compose are you using? – Johann Nov 08 '21 at 17:32
  • I did some tweaking.. given a key for each item in the LazyColumn and checked it in lazyListState.layoutInfo.visibleItemsInfo. This is working but its creating lags on scroll. Any optimized way to use it within LaunchedEffect? – Ali_Waris Nov 09 '21 at 10:25
  • Can you update your code sample so that I can see what you've implemented. Only then can I understand what is causing the lagging. – Johann Nov 09 '21 at 13:18
1

If you want to know if an item is visible you can use the LazyListState#layoutInfo that contains information about the visible items. Since you are reading the state you should use derivedStateOf to avoid redundant recompositions and poor performance

To know if the LazyColumn contains an item you can use:

@Composable
private fun LazyListState.containItem(index:Int): Boolean {

    return remember(this) {
        derivedStateOf {
            val visibleItemsInfo = layoutInfo.visibleItemsInfo
            if (layoutInfo.totalItemsCount == 0) {
                false
            } else {
                visibleItemsInfo.toMutableList().map { it.index }.contains(index)
            }
        }
    }.value
}

Then you can use:

    val state = rememberLazyListState()

    LazyColumn(state = state){
       //items
    }

    //Check for a specific item
    var isItem2Visible = state.containItem(index = 2)

    LaunchedEffect( isItem2Visible){
        if (isItem2Visible)
            //... item visible do something
        else
            //... item not visible do something
    }

If you want to know all the visible items you can use something similar:

@Composable
private fun LazyListState.visibleItems(): List<Int> {

    return remember(this) {
        derivedStateOf {
            val visibleItemsInfo = layoutInfo.visibleItemsInfo
            if (layoutInfo.totalItemsCount == 0) {
                emptyList()
            } else {
                visibleItemsInfo.toMutableList().map { it.index }
            }
        }
    }.value
}
Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841