LaunchedEffect(listState) 和 snapshotFlow { listState.layoutInfo } 的区别:

7 阅读2分钟
LaunchedEffect(listState) 和 snapshotFlow { listState.layoutInfo }中, 为什么LaunchedEffect用listState,而snapshotFlow用listState.layoutInfo?

一句话结论

  • LaunchedEffect(listState) 监听的是:listState 这个对象本身
  • snapshotFlow { listState.layoutInfo } 监听的是:listState 内部的 layoutInfo(滑动布局信息)

为什么要这样?因为它们的用途完全不一样!

1. 先看:LaunchedEffect (listState)

kotlin

LaunchedEffect(listState) {
    // 只在 listState 对象第一次创建或被替换时执行一次
}

它监听的是:listState 这个 “状态实例” 本身

什么时候会变?

  • 页面重建
  • 状态被重新 remember
  • 组件重新创建

它不会因为你滑动列表而执行!滑动不会改变 listState 这个对象,只会改变它内部的值

所以:

LaunchedEffect(listState) 不能监听滑动!

它只能做一次性初始化操作。

2. 再看:snapshotFlow {listState.layoutInfo}

kotlin

snapshotFlow { listState.layoutInfo }
    .collect {
        // 滑动一次,执行一次
    }

它监听的是:listState 内部的 layoutInfo

layoutInfo 里面包含:

  • 总条数
  • 可见条目范围
  • 最后可见位置

只要你手指滑动列表,layoutInfo 就会变!

所以:

snapshotFlow { listState.layoutInfo }专门用来监听滑动!

3. 为什么不能写成 LaunchedEffect (listState.layoutInfo)?

因为:

LaunchedEffect (key) 只会在 key 变化的那一瞬间执行一次

滑动列表时 layoutInfo 会持续变化,但 LaunchedEffect 不会持续响应。

也就是说:

LaunchedEffect 只能 “触发一次”

snapshotFlow 才能 “持续监听”


4. 最本质区别(必背)

表格

写法监听目标能否监听滑动执行次数
LaunchedEffect(listState)listState 对象❌ 不能1 次
snapshotFlow(layoutInfo)滑动布局信息✅ 能滑动 N 次

5. 为什么我们代码要这样写?

kotlin

LaunchedEffect(listState) {
    snapshotFlow { listState.layoutInfo }
        .distinctUntilChanged()
        .collect { ... }
}

原因:

  1. LaunchedEffect(listState) 保证:列表状态创建后,只启动一次监听,不重复创建流。
  2. snapshotFlow { listState.layoutInfo } 保证:滑动时持续收到最新布局信息,实现上拉加载。

6. 终极人话总结

  • LaunchedEffect(listState)  = 打开监听器(只开一次)
  • snapshotFlow(listState.layoutInfo)  = 监听滑动内容(持续接收)
完整代码学习
val listState = rememberLazyListState()
LaunchedEffect(listState) {
    snapshotFlow { listState.layoutInfo }
        .distinctUntilChanged() // 关键:防止重复触发
        .collect { layoutInfo ->
            val totalItems = layoutInfo.totalItemsCount
            val lastVisibleIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1

            // 核心判断:只有 真正滑动到底部 才加载
            val shouldLoad = !isRefreshing
                    && !isLoadingMore
                    && hasMoreData
                    && totalItems > 0
                    && lastVisibleIndex >= totalItems - 2

            if (shouldLoad) {
                isLoadingMore = true
                currentPage++

                // 模拟网络请求
                delay(1200)

                val start = dataList.size + 1
                val newData = (start until start + pageSize).map {
                    DataBean(it, "加载更多 $it")
                }

                dataList = dataList + newData

                // 最多加载5页
                if (currentPage >= 5) hasMoreData = false

                isLoadingMore = false
            }
        }
}