Paging在Android中的应用

3,014 阅读5分钟

1. 简介

为了解决RecyclerView的加载更多的功能,Google推出了JetpackPaging组件。通过Paging组件,可以更流畅无缝的加载更多的数据。

Paging是利用DataSource进行数据的更新。根据用途的不同分为三种。

  1. PagaKeyedDataSource<Key, Value>: 适用于目标数据是按照页数说去数据的场景。key是页数, 请求的数据参数重包含next/previous页数的信息。
  2. ItemKeyedDataSource<Key, Value>: 适用于目标数据的加载依赖特定Item的信息,key包含Item中信息,比如需要根据第N项信息加载第N+1信息。经常应用于评论信息请求。
  3. PositionalDataSource<T>: 适用于目标总数固定,通过特定的位置加载数据,key是位置信息。

添加paging库的依赖:

implementation "androidx.paging:paging-runtime:2.1.1"
implementation "androidx.paging:paging-runtime-ktx:2.1.1"
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'

2. Paging + Room

首先,这部分教程是大家可以熟练使用RoomViewModel为前提,所以不会有对该部分知识进行详细的说明。

2.1 制作数据库

2.1.1 Entity
@Entity(tableName = "users_table")
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Int? = null,

    @ColumnInfo(name = "first_name")
    val firstName: String,

    @ColumnInfo(name = "last_name")
    val lastName: String,

    @ColumnInfo(name = "birthday")
    val birthday: String,

    @ColumnInfo(name = "nationality")
    val nationality: String
)
2.1.2 Dao

这里需要说明的一点是返回值不是List<User>而是DataSource.Factory。 其中左边是key,右边是valuekey是用于内部的PositionalDataSource, 来提示当前数据的位置也就是页数。 而value显而易见的我们需要使用的数据。

@Dao
interface UserDao {
    @Query("SELECT * FROM users_table ORDER BY id ASC")
    fun getAllByLivePage(): DataSource.Factory<Int, User>
}
2.1.3 Database
@Database(version = 1, entities = [User::class])
abstract class UserDataBase : RoomDatabase() {

    companion object {
        private var INSTANCE: UserDataBase? = null

        fun getInstance(context: Context): UserDataBase? {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(
                    context,
                    UserDataBase::class.java,
                    DATABASE_NAME
                ).build()
            }
            return INSTANCE
        }

        fun destroyInstance() {
            INSTANCE = null
        }
        const val DATABASE_NAME = "user_database.db"
    }
    abstract fun getUserDao(): UserDao
}

2.2 制作PagedListAdapter

不同于以往继承RecyclerAdapter或者ListAdapter, 这里我们需要继承PagedListAdapter。 重写的内容跟ListAdapter是一模一样,没有什么特别之处,如下。

class UserAdapter : PagedListAdapter<User, UserAdapter.UserViewHolder>(diffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val binding: ItemUserBinding =
            DataBindingUtil.inflate(inflater, R.layout.item_user, parent, false)

        return UserViewHolder(binding)
    }

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        getItem(position)?.also {
            holder.binding.txtBirthday.text = it.birthday
            holder.binding.txtFirstName.text = it.firstName
            holder.binding.txtLastName.text = it.lastName
            holder.binding.txtNationality.text = it.nationality
        }
    }

    class UserViewHolder(var binding: ItemUserBinding) : RecyclerView.ViewHolder(binding.root)

    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback<User>() {
            override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
                return oldItem === newItem
            }
        }
    }
}

2.3 制作Paging的Config

我们需要在ViewModel中设置PagingConfig。我尝试过在Activity中进行过设置,不能正常运行,不知道我的写法是否哪里问题。

class ShowViewModel(application: Application) : AndroidViewModel(application) {
    val dao =
        UserDataBase.getInstance(
            application.applicationContext
        )?.getUserDao()

    val allUsers = dao!!.getAllByLivePage()
        .toLiveData(Config(pageSize = 10, enablePlaceholders = true, maxSize = 50))
}

从DB中获得的数据要转换成我们熟悉的LiveData,以及同时需要设置Config

  1. pageSize: 一次从数据库加载的数据量。
  2. enablePlaceholders: 如果加载失败的话,是否显示。默认是true
  3. maxSize: 存储在内存中的最大数据量。默认时Int的最大值。
  4. prefetchDistance: 距离最后一个数据有多远的距离时,进行提前获取数据。默认是pageSize
  5. initialLoadSizeHint: 加载到pagedList中的初始数据量。默认是pageSize的3倍。通常是大约正常一页的数据量。

2.4 监视以及传入Adapter

最后一步是在Activity中监视,当数据有变化时传入到Adapter中进行更新。

viewModel.allUsers.observe(this, Observer {
    adapter.submitList(it)
})

2.5 github

RoomAndPageListDemo

3. Paging + Network

讲完Paging + Room以后,我们继续尝试Paging + Network的组合。 首先需要说明一下的是,我没有找到比较合适的Api,所以我在这里进行本地模拟。

还有PagedListAdapter和上面是完全一样的,所以就不再重复赘述了。

3.1 制作DataSource

我们需要继承PageKeyedDataSource<Key,Value>,然后重写三个方法。

我们首先介绍一下重写函数中传入的数据。

  1. params: LoadInitialParams<Int>: 该参数是初始加载的参数。包含两个变量,requestedLoadSize: 要求的数据的量。placeholdersEnabled是跟上面讲到的是一样的。
  2. callback: LoadInitialCallback<Key, Value>: 是初始加载完成后的回调。
  3. params: LoadParams<Int>: 是向前和向后翻页时传入的参数。包含两个变量,requestedLoadSize是跟上面一样,key是当前所在的页数。
  4. callback: LoadCallback<Key, Value>): 是向前和向后翻页完成后的回调。

接下来介绍一下要重写的参数。

  1. loadInitial: 为起始加载。callback的作用是提醒Paging数据已经加载完成。onResult的第一个参数是已加载好的数据。第二参数是前一页的key,如果加载的数据没有前面的数据,则可以设置为null。第三个参数是后一页的key,我这里是已0为起始,所以在这里传入1。
  2. loadBefore: 是向前翻页时的加载。onResult中需要传入两个参数,第一个也是请求获得的数据,第二个数向前加载时的页数,我这里设置的是params.key-1
  3. loadAfter: 是向后翻页时的加载。onResult中也是需要传入两个参数,第一个也是数据,第二个时向后翻页加载时的页数,这里设置的是params.key+1
class UsersDataSource : PageKeyedDataSource<Int, User>() {
    // 起始加载
    override fun loadInitial(
        params: LoadInitialParams<Int>,
        callback: LoadInitialCallback<Int, User>
    ) {

        callback.onResult(getList(0, params.requestedLoadSize), null, 1)
    }
    
    // 向前加载
    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, User>) {
        callback.onResult(getList(params.key, params.requestedLoadSize), params.key - 1)
    }
    // 向后加载
    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, User>) {
        callback.onResult(getList(params.key, params.requestedLoadSize), params.key + 1)
    }
}

3.2 制作DataSourceFactory

我们需用通过DataSource.Factory来生成刚才制作的UsersDataSource。 我们需要继承在上面Paging+Room出现过的DataSourec.Factory<Key, Value>

class CustomPageDataSourceFactory() : DataSource.Factory<Int, User>() {
    override fun create(): DataSource<Int, User> {
        return UsersDataSource()
    }
}

3.3 通过LivePagedListBuilder生成LiveData

最后我们要在Activity中设置PagingLivePagedListBuilder中需要传入两个参数。第一个是DataSource.Factory<Key,Value>,就是上面制作的class。第二个是PagedList.Config,因为上面有介绍所以略过。

// 通过LivePagedListBuilder生成LiveData
val data = LivePagedListBuilder(
        CustomPageDataSourceFactory(),
        PagedList.Config.Builder()
        .setPageSize(20)
        .setInitialLoadSizeHint(60)
        .build()
    ).build()

// 监视数据, 数据有变动时传递给adapter
data.observe(this, Observer {
    adapter.submitList(it)
})

3.4 github

PagingDemo

4. 结论

通过使用Paging库,可以使数据加载无缝进行,能有更好的用户体验。尤其对Room有更好的支持,能减少很多的开发量。