3

I have a lazycolumn with items, and I want to send an event every time one of the items appears on screen. There are examples of events being sent the first time (like here https://plusmobileapps.com/2022/05/04/lazy-column-view-impressions.html) but that example doesn't send events on subsequent times the same item reappears (when scrolling up, for example).

I know it shouldn't be tied to composition, because there can be multiple recompositions while an item remains on screen. What would be the best approach to solve something like this?

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
rohan
  • 523
  • 1
  • 5
  • 22

2 Answers2

2

I modified example in article from keys to index and it works fine, you should check out if there is something wrong with keys matching.

@Composable
private fun MyRow(key: Int, lazyListState: LazyListState, onItemViewed: () -> Unit){
    Text(
        "Row $key",
        color = Color.White,
        modifier = Modifier
            .fillMaxWidth()
            .background(Color.Red)
            .padding(20.dp)
    )

    ItemImpression(key = key, lazyListState = lazyListState) {
        onItemViewed()
    }
}


@Composable
fun ItemImpression(key: Int, lazyListState: LazyListState, onItemViewed: () -> Unit) {

    val isItemWithKeyInView by remember {
        derivedStateOf {
            lazyListState.layoutInfo
                .visibleItemsInfo
                .any { it.index == key }
        }
    }

    if (isItemWithKeyInView) {
        LaunchedEffect(Unit) {
            onItemViewed()
        }
    }
}

Then used it as

LazyColumn(
    verticalArrangement = Arrangement.spacedBy(14.dp),
    state = state
) {
    items(100) {
        MyRow(key = it, lazyListState = state) {
            println(" Item $it is displayed")
            if(it == 11){
                Toast.makeText(context, "item $it is displayed", Toast.LENGTH_SHORT).show()
            }
        }

    }
}

Result

enter image description here

Also instead of sending LazyListState to each ui composable you can move ItemImpression above list as a Composable that only tracks events using state. I put 2, but you can send a list and create for multiple ones either

@Composable
private fun LazyColumnEventsSample() {

    val context = LocalContext.current
    val state = rememberLazyListState()

    ItemImpression(key = 11, lazyListState = state) {
        Toast.makeText(context, "item 11 is displayed", Toast.LENGTH_SHORT).show()
    }


    ItemImpression(key = 13, lazyListState = state) {
        Toast.makeText(context, "item 13 is displayed", Toast.LENGTH_SHORT).show()
    }


    LazyColumn(
        verticalArrangement = Arrangement.spacedBy(14.dp),
        state = state
    ) {
        items(100) {
            Text(
                "Row $it",
                color = Color.White,
                modifier = Modifier
                    .fillMaxWidth()
                    .background(Color.Red)
                    .padding(20.dp)
            )
        }
    }
}
Thracian
  • 43,021
  • 16
  • 133
  • 222
0

The LazyListState#layoutInfo property contains all the information about the visible items. You can use it to know if a specific item is visible in the list.

Something like:

@Composable
private fun LazyListState.containsItem(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 just use something like:

val state = rememberLazyListState()
var isItemVisible = state.containsItem(index = 5)

Then you can observe the value using a side effect:

LaunchedEffect(isItemVisible){
   if (isItemVisible)
      //do something
}

Instead, if you need all the visible items you can use this function to retrieve a List with all the visible items and store it in a variable.

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

enter image description here

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841