ITPub博客

首页 > 移动开发 > ANDROID > 深入浅出:Android Paging3 实现在线与本地数据的高效融合与动态布局解析

深入浅出:Android Paging3 实现在线与本地数据的高效融合与动态布局解析

原创 ANDROID 作者:山有木xi 时间:2024-12-03 14:58:39 0 删除 编辑

0. 前提

摘要

最近项目有个需求是实现类似百度网盘和一刻相册的云盘相册效果,刚好有时间就可以解析一下实现的思路

1. 背景与目标

背景

在开发现代云存储和相册类应用(如百度网盘、一刻相册)中,高效处理在线与本地数据的混合展示,同时实现流畅的分页和动态布局

目标

本文以 Paging3 为核心,结合动态布局和数据混合技术,实现以下目标:

  1. 按时间顺序整合在线与本地数据;
  2. 实现分页加载与动态布局的高效结合;
  3. 解决本地与在线数据交互中的复杂逻辑,如无缝插入、时间区间对齐等。

2. 架构设计: Android Paging3分页与数据处理

核心架构

  • Paging3:用于高效分页加载在线数据;
  • Room:作为本地存储层,支持数据查询和排序;
  • Flow 与 ViewModel:实现数据流的响应式更新。

关键技术点

  1. PagingSource:实现在线数据分页逻辑;
  2. RemoteMediator:桥接在线与本地数据;
  3. GridLayoutManager:支持动态布局调整;
  4. LoadStateListener:动态监听加载状态,优化用户体验。

3. 在线分页的现代实现

PagingSource 的构建

class CloudListPagingSource(private val token: String) : PagingSource<Int, CloudRecord>() {
   override fun getRefreshKey(state: PagingState<Int, CloudPhotoRecord>): Int? {
       return null
   }
   override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CloudPhotoRecord> {
       return try {
           val page = params.key ?: FIRST_PAGE_INDEX
           val response = CloudService.getCloudPage(token, page)
           val responseData = response.data
           if (response.isSuccess && responseData != null) {
               val currentPage = responseData.current
               val prevKey = if (page > FIRST_PAGE_INDEX) currentPage - 1 else null
               val nextKey = if (page < responseData.totalPage) currentPage + 1 else null
               val dataList = mutableListOf<CloudPhotoRecord>()
               
               LoadResult.Page(data = dataList, prevKey = prevKey, nextKey = nextKey)
           } else {
               LoadResult.Error(RuntimeException(response.msg))
           }
       } catch (t: Throwable) {
           Timber.e(t)
           LoadResult.Error(t)
       }
   }
   
   companion object {
       private const val FIRST_PAGE_INDEX = 1
   }
}

配置 Paging 的动态加载

PagingConfig 的参数选择直接影响加载性能:

val pagingFlow = Pager(
    config = PagingConfig(
        pageSize = 30,
        enablePlaceholders = false,
        prefetchDistance = 5
    ),
    pagingSourceFactory = { CloudListPagingSource(token) }
).flow.cachedIn(viewModelScope)

优化:针对大卡片视图的场景建议使用小批量(10-20),而对于网格视图,较大的批量(50-100)更高效。


4. 动态布局与多视图适配

在混合数据场景下,列表需要支持多种布局类型,例如日期标题和内容项。以下是多视图适配的实现:

PagingDataAdapter 的多布局实现

class CloudPagingAdapter : PagingDataAdapter<MyData, RecyclerView.ViewHolder>(DIFF_CALLBACK) {
 
    override fun getItemViewType(position: Int): Int {
        return getItem(position)?.type ?: ViewType.ITEM
    }
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return if (viewType == ViewType.HEADER) {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_header, parent, false)
            HeaderViewHolder(view)
        } else {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_content, parent, false)
            ContentViewHolder(view)
        }
    }
 
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val item = getItem(position)
        if (item != null) {
            if (holder is HeaderViewHolder) {
                holder.bind(item)
            } else if (holder is ContentViewHolder) {
                holder.bind(item)
            }
        }
    }
 
    companion object {
        private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<MyData>() {
            override fun areItemsTheSame(oldItem: MyData, newItem: MyData): Boolean {
                return oldItem.id == newItem.id
            }
 
            override fun areContentsTheSame(oldItem: MyData, newItem: MyData): Boolean {
                return oldItem == newItem
            }
        }
    }
}
 
class HeaderViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    fun bind(data: MyData) {
        // Bind header-specific data to the view
        itemView.findViewById<TextView>(R.id.headerTitle).text = data.content
    }
}
 
class ContentViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    fun bind(data: MyData) {
        // Bind content-specific data to the view
        itemView.findViewById<TextView>(R.id.contentText).text = data.content
    }
}

使用方法如下:

aunch(Dispatchers.IO) {
   cloudViewModel.getCloudListByPage(token).collectLatest {
       cloudPageAdapter.submitData(it)
   }
}

GridLayoutManager 的动态列数调整

根据布局类型动态设置 spanSize:

al layoutManager = GridLayoutManager(context, 4) // 4列网格
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
   override fun getSpanSize(position: Int): Int {
       return if (cloudPageAdapter.getItemViewType(position) == R.layout.item_cloud_photo_title) {
           4
       } else {
           1
       }
   }
}
binding.rvPhoto.layoutManager = layoutManager

5. 在线与本地数据混合的实现

混合策略

在线与本地数据混合需要解决以下四种情况:

  1. 在线无数据时展示本地数据;
  2. 在线有数据,本地数据时间较新;
  3. 在线数据结束后展示剩余本地数据;
  4. 在线数据中间插入本地数据。

关键实现

通过时间戳的逻辑对齐,插入中间范围的数据

val localData = mCloudRecordDao.getRecordsByTimeRange(startTime, endTime)
cloudList.addAll(localData)

通过时间戳的逻辑对齐,实现本地数据的无缝插入(本地数据时间比在线数据新):

if (page == 1 && index == 0) {
   val topTempData = mCloudRecordDao.getRecordsAfter(endTime).sortedByDescending { it.mediaCreateTime }
   val topData = mergeLocalData(topTempData, endTime, cloudPage.date)
   Timber.d("topData==${preTime}==${startTime}===${topData}")
   cloudList.addAll(0, topData)
}

通过时间戳的逻辑对齐,本地数据旧于所有在线数据:

也就是分页最后一页加载时间范围之外的数据

LoadStateListener 的动态监听

监听加载状态,实现动态切换:

cloudPageAdapter.addLoadStateListener(mLoadStateListener)
private val mLoadStateListener: (CombinedLoadStates) -> Unit = {
   when (it.refresh) {
       is LoadState.NotLoading -> {
           mWaitDialog.dismiss()
           Timber.d("mLoadStateListener == LoadState.NotLoading")
           if (cloudPageAdapter.isEmpty()) {
               binding.ivEmpty.isVisible = true
               binding.rvPhoto.isVisible = false
           } else {
               binding.ivEmpty.isVisible = false
               binding.rvPhoto.isVisible = true
           }
       }
       is LoadState.Loading -> {
           mWaitDialog.showWaitDialog("加载资源中")
       }
       is LoadState.Error -> {
           Timber.d("mLoadStateListener == LoadState.Error")
           if (!canLoadLocal) {
               initLocalData()
           } else {
               mWaitDialog.dismiss()
               binding.ivEmpty.isVisible = true
               binding.rvPhoto.isVisible = false
           }
       }
       else -> {}
   }
}

6. 日期标题的插入

日期标题插入逻辑是提升用户体验的重要环节。为确保顺序一致,设置标题时间戳为当天的最大值:

val headerTimestamp = DateUtils.getEndOfDayTimestamp(data.date)

7. 性能优化与总结

  • 性能优化:
    • 使用 cachedIn 避免重复加载;
    • 通过 Room 查询和索引优化本地数据访问。
  • 总结: 本文展示了如何通过 Paging3 高效处理在线与本地数据的混合场景,并结合动态布局实现复杂的 UI 需求。这种架构不仅提升了用户体验,还具有良好的扩展性。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69917874/viewspace-3054485/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
Oracle OCA(Java),IBM高级工程师认证,中国软件行业人才(高级c语言),高级Android工程师,对数据库并发与性能调优也有一定了解

注册时间:2019-04-25

  • 博文量
    147
  • 访问量
    390788