Как реализовать отслеживание показов элементов LazyColumn

У меня есть ленивый столбец с элементами, и я хочу отправлять событие каждый раз, когда один из элементов появляется на экране. Есть примеры событий, отправляемых в первый раз (например, здесь https://plusmobileapps.com/2022/05/04/lazy-column-view-impressions.html), но этот пример не отправляет события в последующие разы одинаково элемент появляется снова (при прокрутке вверх, например).

Я знаю, что это не должно быть связано с композицией, потому что может быть несколько рекомпозиций, пока элемент остается на экране. Что было бы лучшим подходом для решения чего-то подобного?

2 ответа

Я изменил пример в статье с ключей на индекс, и он работает нормально, вы должны проверить, есть ли что-то не так с сопоставлением ключей.

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

Затем использовал его как

      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()
            }
        }

    }
}

Результат

Кроме того, вместо отправки LazyListState в каждый компонуемый пользовательский интерфейс вы можете переместить ItemImpression выше списка как Composable, который отслеживает только события с использованием состояния. Я поставил 2, но вы можете отправить список и создать для нескольких из них либо

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

The LazyListState#layoutInfoсодержит всю информацию о видимых элементах. Вы можете использовать его, чтобы узнать, виден ли конкретный элемент в списке.

Что-то вроде:

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

Затем просто используйте что-то вроде:

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

Затем вы можете наблюдать значение, используя побочный эффект:

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

Вместо этого, если вам нужны все видимые элементы, вы можете использовать эту функцию, чтобы получить список со всеми видимыми элементами и сохранить его в переменной.

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