Android Jetpack全家桶(六)之Paging分页库

Posted by Toeii on July 19, 2019

前言

Android Jetpack是Google在18年IO大会上推荐的一整套组件库,它的出现填补了之前Android中自带的一些缺陷,例如Handler的内存泄露、Camera的不易用性、后台调度难以管理等等。所以我打算把整个架构组件系统性的学习一下,在这里和大家分享,希望能帮助到其他学习者。本系列文章包含十篇:

介绍

Paging分页库可用于一次性数据和块数据显示的加载优化,按需加载部分数据减少网络带宽和系统资源的使用。它可以优化Sql数据的加载、也可以优化Network数据的加载。实际应用中,通常是结合RecyclerView使用。

数据分块加载

Paging分页库的关键组件是PagedList类,它是一个可以分块加载的数据集。在需要更多数据时,可以将其分页到现有的PagedList对象中。 如果任何加载的数据发生更改,则会从LiveData或基于RxJava2的对象向可观察数据持有者发出新的PagedList实例。 PagedList实例在被重新生成后,通过Lifecycle,UI也会随着数据的更新而更新。

数据源

DataSource是数据流入PagedList对象的桥。在构建DataSource中,如果数据源是Room库的话,非常简单的利用DataSource.Factory<Int, T>就可以了。而如果不使用Room库而采用其他的第三方ORM库,则需要自定义DataSource,利用PageKeyedDataSource/ItemKeyedDataSource/PositionalDataSource三者之一即可。

在这里,我使用PageKeyedDataSource提供一个BaseDataSource示例:


class BookRepository private constructor(private val bookDao: BookDao) {

    fun getPageBooks(startIndex:Long,endIndex:Long):List<Book> = BookDao.findBooksByIndexRange(startIndex,endIndex)

}

/**
 * 自定义PageKeyedDataSource
 */
class CustomPageDataSource(private val bookRepository: BookRepository) : PageKeyedDataSource<Int, Book>() {

    private val TAG: String by lazy {
        this::class.java.simpleName
    }

    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Book>) {
        val startIndex = 0L
        val endIndex: Long = 0L + params.requestedLoadSize
        val books = bookRepository.getPageBooks(startIndex, endIndex)

        callback.onResult(books, null, 2)
    }

    // 加载下一页
    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Book>) {
        val startPage = params.key
        val startIndex = ((startPage - 1) * BaseConstant.SINGLE_PAGE_SIZE).toLong() + 1
        val endIndex = startIndex + params.requestedLoadSize - 1
        val books = bookRepository.getPageBooks(startIndex, endIndex)

        callback.onResult(books, params.key + 1)
    }

    // 加载上一页
    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Book>) {
        val startPage = params.key
        val startIndex = ((startPage - 1) * BaseConstant.SINGLE_PAGE_SIZE).toLong() + 1
        val endIndex = startIndex + params.requestedLoadSize - 1
        val books = bookRepository.getPageBooks(startIndex, endIndex)

        callback.onResult(books, params.key - 1)
    }
}

UI

在RecyclerView中设置PagedListAdapter和PagedList类,这些类一起工作以在加载内容时获取和显示内容,预取视图内容并动画内容更改。

结合使用

添加依赖


// room
def room_version = '2.2.0-alpha01'
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
annotationProcessor "android.arch.persistence.room:compiler:$room_version"
kapt "android.arch.persistence.room:compiler:$room_version"

// paging
def paging_version = "2.1.0"
def paging_rxjava_version = "1.0.1"
implementation "android.arch.paging:runtime:$paging_version"
implementation "androidx.paging:paging-runtime-ktx:$paging_version"
implementation "android.arch.paging:rxjava2:$paging_rxjava_version"

// lifecycle
def lifecycle_version = "2.2.0-alpha02"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
annotationProcessor "android.arch.lifecycle:compiler:$lifecycle_version"
kapt "android.arch.lifecycle:compiler:$lifecycle_version"

构建数据源

1,创建Database

@Database(entities = [BookBean::class], version = 1,exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    companion object{
        val CHEESE_DATA = arrayListOf(
            //data ...
        )
        private var instance: AppDatabase? = null

        @Synchronized
        fun get(context: Context):AppDatabase{
            if(null == instance){
                instance = Room.databaseBuilder(context,AppDatabase::class.java,"db_book").build()
            }
            return instance!!
        }
    }

    abstract fun bookDao(): BookDao
    //...Other Dao

}

2,创建Entity

@Entity
data class BookBean(
    @PrimaryKey(autoGenerate = true) var uid: Int,
    @ColumnInfo(name = "book_name") var bookName: String?,
    @ColumnInfo(name = "is_like") var isLike: Boolean?
)

3,创建Dao

@Dao
interface BookDao {

    @Insert
    fun insertAll(books: List<BookBean>)

    @Delete
    fun delete(book: BookBean)

    @Query("SELECT * FROM bookbean")
    fun getAll(): DataSource.Factory<Int,BookBean>

    @Query("SELECT COUNT(*) FROM bookbean ")
    fun getDataCount(): Int

}

4,利用ViewModel构建LiveData


class BookViewModel(app: Application): AndroidViewModel(app) {

    private val dao = AppDatabase.get(app).bookDao()

    companion object {
        private const val PAGE_SIZE = 15
        private const val ENABLE_PLACEHOLDERS = false
    }

    val getAllBook = dao.getAll().toLiveData(Config(
            pageSize = PAGE_SIZE,
            enablePlaceholders = ENABLE_PLACEHOLDERS,
            initialLoadSizeHint = PAGE_SIZE
    ))

    fun insertAll(books: List<String>) = ioThread{
        dao.insertAll(books.map {
            BookBean(uid = 0,bookName = it,isLike = false)
        })
    }

    fun getCount():Int{
        var count = 0
        ioThread{
             count = dao.getDataCount()
        }
        return count
    }

    fun delete(book: BookBean) = ioThread {
         dao.delete(book)
    }

}

5,创建PagedListAdapter


class BookAdapter(private var context: Context): PagedListAdapter<BookBean,RecyclerView.ViewHolder>(diffCallback) {

    var viewModel: BookViewModel? = null

    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback<BookBean>() {
            override fun areItemsTheSame(oldItem: BookBean, newItem: BookBean): Boolean =
                oldItem.uid == newItem.uid

            override fun areContentsTheSame(oldItem: BookBean, newItem: BookBean): Boolean =
                oldItem == newItem
        }
    }

    @SuppressLint("InflateParams")
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view = LayoutInflater.from(context).inflate(R.layout.view_list_book_item,null)
        return BookHolder(view)
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val book= currentList?.get(position)
        if(null != book){
            holder.itemView.tv_book_title.text = book.bookName
            holder.itemView.iv_delete_icon.onClick {
                viewModel?.delete(book)
            }
        }
    }

    class BookHolder(itemView: View): RecyclerView.ViewHolder(itemView)

}

6,结合LifecycleOwner设置UI监听与更新


class BookActivity : AppCompatActivity (){

    lateinit var mBaseModel: BookViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_book)

        mBaseModel = ViewModelProviders.of(this)[BookViewModel::class.java]

        if(mBaseModel.getCount() <=0){
            initData()
        }

        val bookAdapter = BookAdapter(this)
        BookRecyclerView.adapter = bookAdapter
        bookAdapter.viewModel = mBaseModel

        mBaseModel.getAllBook.observe(this, Observer { bookAdapter.submitList(it) }) 
        //PS:这里的this对应的是androidx.appcompat:appcompat:1.1.0-alpha01包下的AppCompatActivity,该AppCompatActivity默认实现了LifecycleOwner。如果是其他包下的AppCompatActivity则需要自己实现LifecycleOwner接口。

    }

    private fun initData() {
        mBaseModel.insertAll(AppDatabase.CHEESE_DATA)
    }

}

结语

Paging的使用场景有很多,该文章介绍了结合RecyclerView和Room的加载场景,这种场景是比较通用的。如果你的使用场景在其他方面,可以通过查阅官方的Samples进行学习。

最后贴出文章中的demo地址,以便参考学习。