zoukankan      html  css  js  c++  java
  • Jetpack系列:Paging组件帮你解决分页加载实现的痛苦

    相信很多小伙伴们在项目实战中,经常会用到界面的分页显示加载更多等功能。需要针对具体功能做针对性开发和调试,耗时耗力。

    Paging组件的使用将这部分的工作简化,从而让开发者更专注于业务的具体实现。下面我们一起来学习下Paging组件的使用方法。


    首先来看下使用Paging组件实现的分页加载和刷新效果:
    ![](https://img2018.cnblogs.com/blog/1820853/201910/1820853-20191010103258092-718449477.gif)

    数据库读取分页加载


    ![](https://img2018.cnblogs.com/blog/1820853/201910/1820853-20191010103309867-715023857.gif)

    网络端分页请求数据

    下面我们针对这两个使用Paging组件的例子进行分析。

    • 数据库读取分页加载示例中,数据一次性获取完成,界面分页显示,按需加载数据,减少了内存资源的使用
    • 网络端分页请求数据,每次请求固定长度的数据信息进行显示,减少网络带宽的使用

    Paging功能的实现用到了Room组件,Room也是Jetpack库的一部分,在SQLite上提供了一个抽象层,为开发者提供了流畅的SQLite数据库访问体验。

    Room简介

    Room组件包含三个主要组成部分:

    • 数据库

    其应该满足四个条件:

    1. 含有@Database注解
    2. 是一个继承自RoomDatabase的抽象类
    3. 注解内包含实体的列表信息
    4. 包含一个返回带@Dao注解类的无参方法
    • 数据实体

    表示数据库中表

    • DAO

    包含用于访问数据库的方法

    应用程序使用Room组件获取与数据库关联的数据访问对象或DAO,然后获取实体,将实体的所有更改同步到数据库。Room三个部分之间的关系如下图:

    ![Room架构图](https://img2018.cnblogs.com/blog/1820853/201910/1820853-20191010104502674-737554548.png)

    Room架构图(引自官方文档)

    Paging的基本使用方法

    Paging组件支持三种不同数据结构:

    • 仅从网络获取
    • 仅从设备数据库获取
    • 两种数据来源的组合,使用设备数据库作为缓存
    ![Paging支持数据架构](https://img2018.cnblogs.com/blog/1820853/201910/1820853-20191010104520872-165043899.png)

    分页库支持数据架构(引自官方文档)

    下面我们以仅从设备数据库获取的方式来了解下Paging分页的基本使用方法。

    环境配置

    首先需要在模块build.gradle中添加对应库支持。

    dependencies {
        versions.room = "2.1.0-alpha06"
        versions.lifecycle = "2.2.0-alpha03"
        versions.paging = "2.1.0-rc01"
        //room数据库访问依赖
        implementation "androidx.room:room-runtime:$versions.room"
        //lifecycle组件依赖,ViewModel
        implementation "androidx.lifecycle:lifecycle-runtime:$versions.lifecycle"
        //paging组件依赖
        implementation "androidx.paging:paging-runtime-ktx:$versions.paging"
        
        kapt "androidx.room:room-compiler:$versions.room"
    }
    

    布局文件

    界面的布局比较简单,主界面包含一个输入框,一个按钮和一个RecyclerView,列表每一项的显示采用卡片式布局,显示文本。

    <androidx.cardview.widget.CardView ...>
        <TextView android:id="@+id/name" .../>
    </androidx.cardview.widget.CardView>
    

    数据准备

    在主Activity进行数据获取和显示前,需要做几点准备工作:

    1. 创建数据实体类Cheese
    2. 创建数据库方法DAO
    3. 创建数据库CheeseDb
    4. 创建自定义CheeseViewModel

    1. 创建实体Cheese

    实体代表了数据库每条数据对象,需要注意必须加@Entity注解

    @Entity
    data class Cheese(@PrimaryKey(autoGenerate = true) val id: Int, val name: String)
    

    此声明创建了一个数据库实体,字段有ID和Name,主键为ID

    2. 创建数据库操作方法DAO

    数据库方法提供了对数据库的基本操作,必须加@Dao注解

    @Dao
    interface CheeseDao {
        @Query("SELECT * FROM Cheese ORDER BY name COLLATE NOCASE ASC")
        fun allCheesesByName(): DataSource.Factory<Int, Cheese>
        @Insert
        fun insert(cheeses: List<Cheese>)
        @Insert
        fun insert(cheese: Cheese)
        @Delete
        fun delete(cheese: Cheese)
    }
    

    此处提供了针对数据库的查询,插入和删除方法,可以看到在查询方法里面会指定数据源类型,当前使用默认类型。Paging还支持如下三种数据源:

    • PageKeyedDataSource

    实现按上下页加载显示

    • ItemKeyedDataSource

    根据上一条数据获取下一条数据

    • PositionalDataSource

    从指定位置开始加载

    关于这三种数据源的高级使用方法,请参考官方文档说明示例

    3. 创建数据库

    数据库为界面显示提供了数据支持,当前示例程序中,数据库创建时,插入了预置数据。

    • 必须加@Database注解

    • 必须声明数据列表信息

    • 必须含有无参抽象方法,返回带@Dao注解的类

    • 必须为抽象类,且继承RoomDatabase

    @Database(entities = arrayOf(Cheese::class), version = 1)
    abstract class CheeseDb : RoomDatabase() {
        abstract fun cheeseDao(): CheeseDao//返回DAO
        ...
        //获取数据库实例,同步且单例
        @Synchronized
        fun get(context: Context): CheeseDb {
            if (instance == null) {
                instance = Room.databaseBuilder(context.applicationContext,
                        CheeseDb::class.java, "CheeseDatabase")
                        .addCallback(object : RoomDatabase.Callback() {
                            override fun onCreate(db: SupportSQLiteDatabase) {
                                //数据库创建时插入预置数据
                                fillInDb(context.applicationContext)
                            }
                        }).build()
            }
            return instance!!
        }
        
        private fun fillInDb(context: Context) {
            // inserts in Room are executed on the current thread, so we insert in the background
            // CHEESE_DATA为默认数据列表
            ioThread {
                get(context).cheeseDao().insert(
                        CHEESE_DATA.map { Cheese(id = 0, name = it) })
            }
        }
    }
    
    

    4. 创建ViewModel

    创建自定义ViewModel为界面和数据提供处理支持。其包含了DAO,数据列表信息等。

    class CheeseViewModel(app: Application) : AndroidViewModel(app) {
        val dao = CheeseDb.get(app).cheeseDao()
        val allCheeses = dao.allCheesesByName().toLiveData(Config(
            pageSize = 30,//指定页面显示的数据项数量
            enablePlaceholders = true,//是否允许使用占位符
            maxSize = 200 //一次性加载数据的最大数量
          ),
          fetchExecutor = Executor {  }//自定义Executor更好地控制paging库何时从应用程序的数据库中加载列表
        )
        
        fun insert(text: CharSequence) = ioThread {
            dao.insert(Cheese(id = 0, name = text.toString()))
        }
    
        fun remove(cheese: Cheese) = ioThread {
            dao.delete(cheese)
        }
    }
    

    小提示:自定义ViewModel直接继承AndroidViewModel,可以在其中做一些依赖于Context的资源获取等功能。

    public class AndroidViewModel extends ViewModel {
        ...
        public <T extends Application> T getApplication() {
            return (T) mApplication;
        }
    }
    

    ViewModel的创建,包含了数据的获取和更新:

    • 通过DAO获取数据库的数据列表
    • 使用LiveData组件管理数据
    • 增加分页支持(pageSize,enablePlaceholders,maxSize)功能
    • 增加自定义Executor

    Paging组件是依赖页面长度、占位符、最大长度三个属性来进行小块数据加载显示的。

    页面大小:每页显示的实体数量

    最大长度:也称预取长度,此值应为pageSize的几倍大小(具体项目可根据实际情况调试)

    占位符:如果设置为true,则为尚未完成加载的列表项显示占位符

    占位符的使用需要有可数的数据集合,默认显示效果,数据项有相同大小的视图显示,有以下优点:

    • 提供完整滚动条支持
    • 无需显示加载更多项

    界面绑定

    数据已经准备好了,下面开始和界面进行绑定显示。

    界面显示时,需要提供与RecyclerView绑定的adapter,需要注意使用Paging进行分页加载,adapter需要继承自PagedListAdapter。

    
    class CheeseAdapter : PagedListAdapter<Cheese, CheeseViewHolder>(diffCallback) {
        override fun onBindViewHolder(holder: CheeseViewHolder, position: Int) {
            holder.bindTo(getItem(position))
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheeseViewHolder =
                CheeseViewHolder(parent)
        companion object {
            //根据diffCallback来确认新加载的数据是否与旧数据有差异,确定是否更新显示
            private val diffCallback = object : DiffUtil.ItemCallback<Cheese>() {
                override fun areItemsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
                        oldItem.id == newItem.id
                //kotlin使用==会将对象的内容进行对比,使用java需要重写equals方法并替换
                override fun areContentsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
                        oldItem == newItem
            }
        }
    }
    
    //ViewHolder的实现比较简单,将Cheese数据更新到TextView
    class CheeseViewHolder(parent :ViewGroup) : RecyclerView.ViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.cheese_item, parent, false)) {
    
        private val nameView = itemView.findViewById<TextView>(R.id.name)
        var cheese : Cheese? = null
    
        //未绑定数据,或者打开占位符后快速滑动会出现cheese为null,实际项目中需要
        //处理此种情况,数据加载时会重新rebind
        fun bindTo(cheese : Cheese?) {
            this.cheese = cheese
            nameView.text = cheese?.name
        }
    }
    
    
    class MainActivity : AppCompatActivity() {
    private val viewModel by viewModels<CheeseViewModel>()//创建viewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val adapter = CheeseAdapter()//继承PagedListAdapter的类对象
        cheeseList.adapter = adapter //为RecyclerView添加适配器
        //viewmodel数据与adapter绑定,在数据变化时通知adapter更新UI
        viewModel.allCheeses.observe(this, Observer(adapter::submitList))
        initSwipeToDelete()//设置左滑/右滑删除数据项
        initAddButtonListener()//设置点击添加Cheese功能
        ...
    }
    

    好了,大功告成!

    最终效果



    你也可以尝试使用仅网络或网络+数据库的方式进行功能开发。

    源码在此:


    数据库分页

    网络请求分页


    欢迎关注公众号,留言讨论更多技术问题
    ![file](https://img2018.cnblogs.com/blog/1820853/201910/1820853-20191010103323866-283198181.jpg)
  • 相关阅读:
    ecshop 商品分类下的销售排行
    ecshop批量清除商品的精品新品热销属性
    ECSHOP二次开发之给商品增加新字段
    ECSHOP首页调用文章内的缩略图
    ECSHOP给分类添加代表图
    ECSHOP首页促销商品下显示促销时间
    鼠标点击后更换背景
    ECSHOP如何修改商品评论或留言的日期
    ECSHOP设置指定IP才能登录后台
    ecshop远程图片本地化保存相册图片
  • 原文地址:https://www.cnblogs.com/danvie/p/11646325.html
Copyright © 2011-2022 走看看