How to implement LazyColumn item impression tracking

752 Views Asked by At

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?

2

There are 2 best solutions below

0
Thracian On BEST ANSWER

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)
            )
        }
    }
}
0
Gabriele Mariotti On

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