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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM