🌟

Jetpack ComposeのLazyColumnで効率的なScroll Listenerの実装方法

2022/06/10に公開

概要

Jetpack Composeでのスクロールリスナー的なものを何も考えずに実装したら、何度も呼ばれちゃって非効率だったので、それをちゃんと書くための記事です。

関係する知識は以下です。この説明になります。

  • rememberLazyListState()
  • LaunchedEffect
  • snapshotFlow
  • derivedStateOf

スクロールしたときに何らかの条件によってログを送信するサンプル実装で説明します。

スクロールの状態の取得

rememberLazyListState()

まずは、スクロールの状態を取得するための実装です。
rememberLazyListState()の結果を LazyColumnに渡すだけです。
これで、変数listStateにスクロールの状態を保持できます。

    val listState = rememberLazyListState()
    LazyColumn(state = listState) {
        // ...
    }

無駄な再コンポーズを避ける実装

LaunchedEffect

普通にlistState.layoutInfoにアクセスすると再コンポーズを永遠にし続けます。

  LazyColumn(state = listState) {
        // ...
  }
  if (listState.layoutInfo.visibleItemsInfo.size > 0) {
    println("無限に呼ばれる")
  }

それを避けるためにコンポジションのタイミングだけ呼ぶようにするため、LaunchedEffectコンポーザブル関数を使います。

LaunchedEffect(listState) {
・・・
}

スクロールの変更を監視

snapshotFlow()

このままだとコンポジションされたときの一度しか呼ばれないので、snapshotFlow()を利用します。
snapshotFlow()の仕組みは、引数のブロック内で利用しているState(この場合は、listState.layoutInfo)が変更されたときに、再度引数のblockを呼び出すようになっています。

  LaunchedEffect(listState) {
    snapshotFlow {
      println("call snapshotFlow block")
      listState.layoutInfo
    }.collect {
    }
  }

この状態でスクロールすると「call snapshotFlow block」が出力されます。(listState.layoutInfoを呼び出さないとsnapshotFlowのblockは呼び出されません)

スクロール条件によってログを送信する

以前の記事に書いたlistState.layoutInfolistState.layoutInfo.visibleItemsInfo[index]を使って、アイテムが7割表示されたら、ログを送信するロジックです。
ポイントは、distinctUntilChanged()で同じ状態では送らないようにして、filter()によってログ送信する条件を指定しています。

  LaunchedEffect(listState) {
    snapshotFlow {
      val itemHeight = listState.layoutInfo.visibleItemsInfo.last().size
      val itemTop = listState.layoutInfo.visibleItemsInfo.last().offset
      val deviceHeight = listState.layoutInfo.viewportSize.height
      val viewableTopPixel = deviceHeight - itemTop
      val isTarget = viewableTopPixel > itemHeight * 0.7
      Pair(isTarget, listState.layoutInfo.visibleItemsInfo.last().index)
    }.distinctUntilChanged()
      .filter { it.first }
      .collect {
        // ログ送信
        Analytics().sendViewableLog(it.second)
      }
  }
NewsPicks の Zenn

Discussion