You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Kotlin为加载Firebase图片的RecyclerView实现分页(含类Pinterest预加载)

现有代码核心问题
  • 滚动监听逻辑错误:firstVisibleItems 初始化为null,无法获取第一个可见项位置,分页触发条件永远不会生效
  • Firebase数据拉取逻辑错误:使用全量实时监听,每次加载都会清空已有数据重新拉取全部内容,没有实现分页拉取
  • 状态逻辑错误:数据加载完成后将loading设为true,导致后续分页触发逻辑被拦截
  • 缺少滚动速度检测逻辑,无法实现快慢滚动的差异化交互
基础分页实现(需求1)

Firebase Realtime Database 分页依赖游标实现,每次拉取指定数量的数据,记录最后一条数据的key作为下一次拉取的起始位置,避免全量拉取。

类Pinterest交互实现(需求2)

新增两项逻辑即可实现对应效果:

  • 预加载阈值:设置为剩余10项时触发下一次拉取,保证慢速滚动时用户还未滑动到列表末尾就已完成数据加载,无感知
  • 滚动速度检测:通过滚动位移与时间差计算滚动速度,仅当滚动速度超过阈值时显示加载进度条,慢速滚动时隐藏加载状态

修正后完整代码

Home_Fragment.kt

// 全局变量定义
private val PAGE_SIZE = 20
private val PRELOAD_THRESHOLD = 10 // 剩余10项时触发预加载
private val FAST_SCROLL_THRESHOLD = 2000 // 快速滚动阈值,单位px/s,可自行调整
private var isLoading = false
private var lastKey: String? = null // 分页游标,记录最后一条数据的key
private var isLastPage = false
private var lastScrolledTime = System.currentTimeMillis()
private lateinit var postsAdapter: PostAdapter_Home
private lateinit var mUploads: MutableList<Upload?>

// 在onCreateView中初始化
override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    val view = inflater.inflate(R.layout.fragment_home, container, false)
    val staggeredGridLayoutManager = StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL)
    postRecyclerView.layoutManager = staggeredGridLayoutManager
    mUploads = ArrayList()
    postsAdapter = PostAdapter_Home(requireContext(), mUploads)
    postRecyclerView.adapter = postsAdapter

    // 滚动监听
    postRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            // 计算滚动速度
            val currentTime = System.currentTimeMillis()
            val timeDiff = currentTime - lastScrolledTime
            var scrollSpeed = 0
            if (timeDiff > 0) {
                scrollSpeed = Math.abs(dy) * 1000 / timeDiff
            }
            lastScrolledTime = currentTime

            val layoutManager = recyclerView.layoutManager as StaggeredGridLayoutManager
            val visibleItemCount = layoutManager.childCount
            val totalItemCount = layoutManager.itemCount
            val firstVisibleItems = IntArray(layoutManager.spanCount)
            layoutManager.findFirstVisibleItemPositions(firstVisibleItems)
            val firstVisibleItem = firstVisibleItems.maxOrNull() ?: 0

            // 预加载触发条件:未加载、未到最后一页、剩余项小于预加载阈值、向上滑动
            if (!isLoading && !isLastPage && dy > 0 && (totalItemCount - visibleItemCount) <= (firstVisibleItem + PRELOAD_THRESHOLD)) {
                // 仅快速滚动时显示加载进度条
                if (scrollSpeed > FAST_SCROLL_THRESHOLD) {
                    // 此处替换为你自己的加载进度条控件显示逻辑
                    loadingProgressBar.visibility = View.VISIBLE
                }
                loadNextPage()
            }
        }
    })

    // 加载第一页数据
    loadFirstPage()
    shimmerFrameLayout.startShimmer()
    return view
}

// 加载第一页数据
private fun loadFirstPage() {
    isLoading = true
    databaseReference.orderByKey().limitToFirst(PAGE_SIZE).addListenerForSingleValueEvent(object : ValueEventListener {
        override fun onDataChange(snapshot: DataSnapshot) {
            mUploads.clear()
            if (snapshot.exists()) {
                for (dataSnapshot in snapshot.children) {
                    val upload = dataSnapshot.getValue(Upload::class.java)!!
                    upload.setmKey(dataSnapshot.key)
                    mUploads.add(upload)
                    lastKey = dataSnapshot.key
                }
            }
            isLoading = false
            shimmerFrameLayout.stopShimmer()
            shimmerFrameLayout.visibility = View.GONE
            postsAdapter.notifyDataSetChanged()
            isLastPage = mUploads.size < PAGE_SIZE
        }

        override fun onCancelled(error: DatabaseError) {
            isLoading = false
            shimmerFrameLayout.stopShimmer()
            shimmerFrameLayout.visibility = View.GONE
        }
    })
}

// 加载下一页数据
private fun loadNextPage() {
    isLoading = true
    lastKey?.let { cursor ->
        databaseReference.orderByKey().startAfter(cursor).limitToFirst(PAGE_SIZE).addListenerForSingleValueEvent(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                val newItems = mutableListOf<Upload?>()
                if (snapshot.exists()) {
                    for (dataSnapshot in snapshot.children) {
                        val upload = dataSnapshot.getValue(Upload::class.java)!!
                        upload.setmKey(dataSnapshot.key)
                        newItems.add(upload)
                        lastKey = dataSnapshot.key
                    }
                }
                isLoading = false
                // 隐藏加载进度条
                loadingProgressBar.visibility = View.GONE
                if (newItems.isNotEmpty()) {
                    val insertStart = mUploads.size
                    mUploads.addAll(newItems)
                    postsAdapter.notifyItemRangeInserted(insertStart, newItems.size)
                }
                isLastPage = newItems.size < PAGE_SIZE
            }

            override fun onCancelled(error: DatabaseError) {
                isLoading = false
                loadingProgressBar.visibility = View.GONE
            }
        })
    } ?: run {
        isLoading = false
        isLastPage = true
    }
}

优化说明

  1. 替换addValueEventListener为addListenerForSingleValueEvent,避免数据变动时全量回调打乱分页逻辑,若需要实时更新数据可单独监听新增条目追加到列表
  2. Glide缓存策略可调整为DiskCacheStrategy.ALL,提升图片加载速度,进一步降低加载感知
  3. 若需要添加底部加载脚,可在Adapter中新增加载项类型,根据isLoading状态控制显示隐藏

内容的提问来源于stack exchange,提问作者Vasant Raval

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 08:54:01