6. Jetpack---Paging你知道怎樣上拉加載嗎?

之前的幾篇源碼分析我們分別對NavigationLifecyclesViewModelLiveData、進行了分析,也對JetPack有了更深入的瞭解。但是Jetpack遠不止這些組件,今天的主角—Paging,Jetpack中的分頁組件,官方是這麼形容它的:‘’逐步從您的數據源按需加載信息‘’

如果你對Jetpack組件有了解或者想對源碼有更深入的瞭解,請看我之前的幾篇文章:

1. Jetpack源碼解析—看完你就知道Navigation是什麼了?

2. Jetpack源碼解析—Navigation爲什麼切換Fragment會重繪?

3. Jetpack源碼解析—用Lifecycles管理生命週期

4. Jetpack源碼解析—LiveData的使用及工作原理

5. Jetpack源碼解析—ViewModel基本使用及源碼解析

1. 背景

在我的Jetpack_Note系列中,對每一篇的分析都有相對應的代碼片段及使用,我把它做成了一個APP,目前功能還不完善,代碼我也上傳到了GitHub上,參考了官方的Demo以及目前網上的一些文章,有興趣的小夥伴可以看一下,別忘了給個Star。

https://github.com/Hankkin/JetPack_Note

今天我們的主角是Paging,介紹之前我們先看一下效果:

19-08-10-11-18-59.2019-08-10 11_33_51

2. 簡介

2.1 基本介紹

官方定義:

分頁庫Pagin LibraryJetpack的一部分,它可以妥善的逐步加載數據,幫助您一次加載和顯示一部分數據,這樣的按需加載可以減少網絡貸款和系統資源的使用。分頁庫支持加載有限以及無限的list,比如一個持續更新的信息源,分頁庫可以與RecycleView無縫集合,它還可以與LiveData或RxJava集成,觀察界面中的數據變化。

2.2 核心組件

1. PagedList

PageList是一個集合類,它以分塊的形式異步加載數據,每一塊我們稱之爲。它繼承自AbstractList,支持所有List的操作,它的內部有五個主要變量:

  1. mMainThreadExecutor 主線程Executor,用於將結果傳遞到主線程
  2. mBackgroundThreadExecutor 後臺線程,執行負載業務邏輯
  3. BoundaryCallback 當界面顯示緩存中靠近結尾的數據的時候,它將加載更多的數據
  4. Config PageList從DataSource中加載數據的配置
  5. PagedStorage 用於存儲加載到的數據

Config屬性:

  1. pageSize:分頁加載的數量
  2. prefetchDistance:預加載的數量
  3. initialLoadSizeHint:初始化數據時加載的數量,默認爲pageSize*3
  4. enablePlaceholders:當item爲null是否使用placeholder顯示

PageList會通過DataSource加載數據,通過Config的配置,可以設置一次加載的數量以及預加載的數量。除此之外,PageList還可以想RecycleView.Adapter發送更新的信號,驅動UI的刷新。

2. DataSource

DataSource<Key,Value> 顧名思義就是數據源,它是一個抽象類,其中Key對應加載數據的條件信息,Value對應加載數據的實體類。Paging庫中提供了三個子類來讓我們在不同場景的情況下使用:

  1. PageKeyedDataSource:如果後端API返回數據是分頁之後的,可以使用它;例如:官方Demo中GitHub API中的SearchRespositories就可以返回分頁數據,我們在GitHub API的請求中制定查詢關鍵字和想要的哪一頁,同時也可以指明每個頁面的項數。
  2. ItemKeyedDataSource:如果通過鍵值請求後端數據;例如我們需要獲取在某個特定日期起Github的前100項代碼提交記錄,該日期將成爲DataSource的鍵,ItemKeyedDataSource允許自定義如何加載初始頁;該場景多用於評論信息等類似請求
  3. PositionalDataSource:適用於目標數據總數固定,通過特定的位置加載數據,這裏Key是Integer類型的位置信息,T即Value。 比如從數據庫中的1200條開始加在20條數據。

3. PagedListAdapter

PageListAdapter繼承自RecycleView.Adapter,和RecycleView實現方式一樣,當數據加載完畢時,通知RecycleView數據加載完畢,RecycleView填充數據;當數據發生變化時,PageListAdapter會接受到通知,交給委託類AsyncPagedListDiffer來處理,AsyncPagedListDiffer是對**DiffUtil.ItemCallback**持有對象的委託類,AsyncPagedListDiffer使用後臺線程來計算PagedList的改變,item是否改變,由DiffUtil.ItemCallback決定。

3.基本使用

3.1 添加依賴包

implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx
    implementation "androidx.paging:paging-runtime-ktx:$paging_version" // For Kotlin use paging-runtime-ktx
    // alternatively - without Android dependencies for testing
    testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx
    // optional - RxJava support
    implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx

3.2 PagingWithRoom使用

新建UserDao

/**
 * created by Hankkin
 * on 2019-07-19
 */
@Dao
interface UserDao {


    @Query("SELECT * FROM User ORDER BY name COLLATE NOCASE ASC")
    fun queryUsersByName(): DataSource.Factory<Int, User>

    @Insert
    fun insert(users: List<User>)

    @Insert
    fun insert(user: User)

    @Delete
    fun delete(user: User)

}

創建UserDB數據庫

/**
 * created by Hankkin
 * on 2019-07-19
 */
@Database(entities = arrayOf(User::class), version = 1)
abstract class UserDB : RoomDatabase() {

    abstract fun userDao(): UserDao
    companion object {
        private var instance: UserDB? = null
        @Synchronized
        fun get(context: Context): UserDB {
            if (instance == null) {
                instance = Room.databaseBuilder(context.applicationContext,
                    UserDB::class.java, "UserDatabase")
                    .addCallback(object : RoomDatabase.Callback() {
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            fillInDb(context.applicationContext)
                        }
                    }).build()
            }
            return instance!!
        }

        /**
         * fill database with list of cheeses
         */
        private fun fillInDb(context: Context) {
            // inserts in Room are executed on the current thread, so we insert in the background
            ioThread {
                get(context).userDao().insert(
                    CHEESE_DATA.map { User(id = 0, name = it) })
            }
        }
    }
}

創建PageListAdapter

/**
 * created by Hankkin
 * on 2019-07-19
 */
class PagingDemoAdapter : PagedListAdapter<User, PagingDemoAdapter.ViewHolder>(diffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        ViewHolder(AdapterPagingItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = getItem(position)
        holder.apply {
            bind(createOnClickListener(item), item)
            itemView.tag = item
        }
    }

    private fun createOnClickListener(item: User?): View.OnClickListener {
        return View.OnClickListener {
            Toast.makeText(it.context, item?.name, Toast.LENGTH_SHORT).show()
        }
    }


    class ViewHolder(private val binding: AdapterPagingItemBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(listener: View.OnClickListener, item: User?) {
            binding.apply {
                clickListener = listener
                user = item
                executePendingBindings()
            }
        }
    }

    companion object {
        /**
         * This diff callback informs the PagedListAdapter how to compute list differences when new
         * PagedLists arrive.
         * <p>
         * When you add a Cheese with the 'Add' button, the PagedListAdapter uses diffCallback to
         * detect there's only a single item difference from before, so it only needs to animate and
         * rebind a single view.
         *
         * @see android.support.v7.util.DiffUtil
         */
        private val diffCallback = object : DiffUtil.ItemCallback<User>() {
            override fun areItemsTheSame(oldItem: User, newItem: User): Boolean =
                oldItem.id == newItem.id

            /**
             * Note that in kotlin, == checking on data classes compares all contents, but in Java,
             * typically you'll implement Object#equals, and use it to compare object contents.
             */
            override fun areContentsTheSame(oldItem: User, newItem: User): Boolean =
                oldItem == newItem
        }
    }
}

ViewModel承載數據

class PagingWithDaoViewModel internal constructor(private val pagingRespository: PagingRespository) : ViewModel() {

    val allUsers = pagingRespository.getAllUsers()

    fun insert(text: CharSequence) {
        pagingRespository.insert(text)
    }

    fun remove(user: User) {
        pagingRespository.remove(user)
    }
}

Activity中觀察到數據源的變化後,會通知Adapter自動更新數據

class PagingWithDaoActivity : AppCompatActivity() {

    private lateinit var viewModel: PagingWithDaoViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_paging_with_dao)
        setLightMode()
        setupToolBar(toolbar) {
            title = resources.getString(R.string.paging_with_dao)
            setDisplayHomeAsUpEnabled(true)
        }
        viewModel = obtainViewModel(PagingWithDaoViewModel::class.java)
        val adapter = PagingDemoAdapter()
        rv_paging.adapter = adapter
        viewModel.allUsers.observe(this, Observer(adapter::submitList))
    }


    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        when (item?.itemId) {
            android.R.id.home -> finish()
        }
        return super.onOptionsItemSelected(item)
    }
}

3.3 PagingWithNetWork 使用

上面我們通過Room進行了數據庫加載數據,下面看一下通過網絡請求記載列表數據:

和上面不同的就是Respository數據源的加載,之前我們是通過Room加載DB數據,現在我們要通過網絡獲取數據:

GankRespository 乾貨數據源倉庫

/**
 * created by Hankkin
 * on 2019-07-30
 */
class GankRespository {

    companion object {

        private const val PAGE_SIZE = 20

        @Volatile
        private var instance: GankRespository? = null

        fun getInstance() =
            instance ?: synchronized(this) {
                instance
                    ?: GankRespository().also { instance = it }
            }
    }

    fun getGank(): Listing<Gank> {
        val sourceFactory = GankSourceFactory()
        val config = PagedList.Config.Builder()
            .setPageSize(PAGE_SIZE)
            .setInitialLoadSizeHint(PAGE_SIZE * 2)
            .setEnablePlaceholders(false)
            .build()
        val livePageList = LivePagedListBuilder<Int, Gank>(sourceFactory, config).build()
        val refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) { it.initialLoad }
        return Listing(
            pagedList = livePageList,
            networkState = Transformations.switchMap(sourceFactory.sourceLiveData) { it.netWorkState },
            retry = { sourceFactory.sourceLiveData.value?.retryAllFailed() },
            refresh = { sourceFactory.sourceLiveData.value?.invalidate() },
            refreshState = refreshState
        )
    }

}

可以看到getGank()方法返回了Listing,那麼Listing是個什麼呢?

/**
 * Data class that is necessary for a UI to show a listing and interact w/ the rest of the system
 * 封裝需要監聽的對象和執行的操作,用於上拉下拉操作
 * pagedList : 數據列表
 * networkState : 網絡狀態
 * refreshState : 刷新狀態
 * refresh : 刷新操作
 * retry : 重試操作
 */
data class Listing<T>(
        // the LiveData of paged lists for the UI to observe
    val pagedList: LiveData<PagedList<T>>,
        // represents the network request status to show to the user
    val networkState: LiveData<NetworkState>,
        // represents the refresh status to show to the user. Separate from networkState, this
        // value is importantly only when refresh is requested.
    val refreshState: LiveData<NetworkState>,
        // refreshes the whole data and fetches it from scratch.
    val refresh: () -> Unit,
        // retries any failed requests.
    val retry: () -> Unit)

Listing是我們封裝的一個數據類,將數據源、網絡狀態、刷新狀態、下拉刷新操作以及重試操作都封裝進去了。那麼我們的數據源從哪裏獲取呢,可以看到Listing的第一個參數pageList = livePageList,livePageList通過LivePagedListBuilder創建,LivePagedListBuilder需要兩個參數(DataSource,PagedList.Config):

GankSourceFactory

/**
 * created by Hankkin
 * on 2019-07-30
 */
class GankSourceFactory(private val api: Api = Injection.provideApi()) : DataSource.Factory<Int, Gank>(){

    val sourceLiveData = MutableLiveData<GankDataSource>()

    override fun create(): DataSource<Int, Gank> {
        val source = GankDataSource(api)
        sourceLiveData.postValue(source)
        return  source
    }
}

GankDataSource


/**
 * created by Hankkin
 * on 2019-07-30
 */
class GankDataSource(private val api: Api = Injection.provideApi()) : PageKeyedDataSource<Int, Gank>() {

    private var retry: (() -> Any)? = null
    val netWorkState = MutableLiveData<NetworkState>()
    val initialLoad = MutableLiveData<NetworkState>()

    fun retryAllFailed() {
        val prevRetry = retry
        retry = null
        prevRetry?.also { it.invoke() }
    }

    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Gank>) {
        initialLoad.postValue(NetworkState.LOADED)
        netWorkState.postValue(NetworkState.HIDDEN)
        api.getGank(params.requestedLoadSize, 1)
            .enqueue(object : Callback<GankResponse> {
                override fun onFailure(call: Call<GankResponse>, t: Throwable) {
                    retry = {
                        loadInitial(params, callback)
                    }
                    initialLoad.postValue(NetworkState.FAILED)
                }

                override fun onResponse(call: Call<GankResponse>, response: Response<GankResponse>) {
                    if (response.isSuccessful) {
                        retry = null
                        callback.onResult(
                            response.body()?.results ?: emptyList(),
                            null,
                            2
                        )
                        initialLoad.postValue(NetworkState.LOADED)
                    } else {
                        retry = {
                            loadInitial(params, callback)
                        }
                        initialLoad.postValue(NetworkState.FAILED)
                    }
                }

            })
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Gank>) {
        netWorkState.postValue(NetworkState.LOADING)
        api.getGank(params.requestedLoadSize, params.key)
            .enqueue(object : Callback<GankResponse> {
                override fun onFailure(call: Call<GankResponse>, t: Throwable) {
                    retry = {
                        loadAfter(params, callback)
                    }
                    netWorkState.postValue(NetworkState.FAILED)
                }

                override fun onResponse(call: Call<GankResponse>, response: Response<GankResponse>) {
                    if (response.isSuccessful) {
                        retry = null
                        callback.onResult(
                            response.body()?.results ?: emptyList(),
                            params.key + 1
                        )
                        netWorkState.postValue(NetworkState.LOADED)
                    } else {
                        retry = {
                            loadAfter(params, callback)
                        }
                        netWorkState.postValue(NetworkState.FAILED)
                    }
                }

            })
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Gank>) {
    }


}

網絡請求的核心代碼在GankDataSource中,因爲我們的請求是分頁請求,所以這裏的GankDataSource我們繼承自PageKeyedDataSource,它實現了三個方法:

loadInitial: 初始化加載,初始加載的數據 也就是我們直接能看見的數據

loadAfter: 下一頁加載,每次傳遞的第二個參數 就是 你加載數據依賴的key

loadBefore: 往上滑加載的數據

可以看到我們在loadInitial中設置了initialLoadnetWorkState的狀態值,同時通過RetrofitApi獲取網絡數據,並在成功和失敗的回調中對數據和網絡狀態值以及加載初始化做了相關的設置,具體就不介紹了,可看代碼。loadAfter同理,只不過我們在加載數據後對key也就是我們的page進行了+1操作。

Config參數就是我們對分頁加載的一些配置:

val config = PagedList.Config.Builder()
            .setPageSize(PAGE_SIZE)
            .setInitialLoadSizeHint(PAGE_SIZE * 2)
            .setEnablePlaceholders(false)
            .build()

下面看我們在Activity中怎樣使用:

PagingWithNetWorkActivity

class PagingWithNetWorkActivity : AppCompatActivity() {

    private lateinit var mViewModel: PagingWithNetWorkViewModel
    private lateinit var mDataBinding: ActivityPagingWithNetWorkBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mDataBinding = DataBindingUtil.setContentView(this,R.layout.activity_paging_with_net_work)
        setLightMode()
        setupToolBar(toolbar) {
            title = resources.getString(R.string.paging_with_network)
            setDisplayHomeAsUpEnabled(true)
        }
        mViewModel = obtainViewModel(PagingWithNetWorkViewModel::class.java)
        mDataBinding.vm = mViewModel
        mDataBinding.lifecycleOwner = this

        val adapter = PagingWithNetWorkAdapter()
        mDataBinding.rvPagingWithNetwork.adapter = adapter
        mDataBinding.vm?.gankList?.observe(this, Observer { adapter.submitList(it) })
        mDataBinding.vm?.refreshState?.observe(this, Observer {
            mDataBinding.rvPagingWithNetwork.post {
                mDataBinding.swipeRefresh.isRefreshing = it == NetworkState.LOADING
            }
        })

        mDataBinding.vm?.netWorkState?.observe(this, Observer {
            adapter.setNetworkState(it)
        })
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        when (item?.itemId) {
            android.R.id.home -> finish()
        }
        return super.onOptionsItemSelected(item)
    }
}

ViewModel中的gankList是一個LiveData,所以我們在這裏給它設置一個觀察,當數據變動是調用adapter.submitList(it),刷新數據,這個方法是PagedListAdapter中的,裏面回去檢查新數據和舊數據是否相同,也就是上面我們提到的AsyncPagedListDiffer來實現的。到這裏整個流程就已經結束了,想看源碼可以到Github上。

4. 總結

我們先看下官網給出的gif圖:

[外鏈圖片轉存失敗(img-eFq85sdR-1566831114700)(https://note.youdao.com/yws/api/personal/file/WEBd1ac1c87130f18afd376a4f7fb273bb0?method=download&shareKey=460a039c8e8695464d321519258a104b)]

總結一下,Paging的基本原理爲:

  1. 使用DataSource從網絡或者數據庫獲取數據
  2. 將數據保存到PageList中
  3. 將PageList中的數據提交給PageListAdapter
  4. PageListAdapter在後臺線程中通過Diff對比新老數據,反饋到RecycleView中
  5. RecycleView刷新數據

基本原理在圖上我們可以很清晰的瞭解到了,本篇文章的Demo中結合了ViewModel以及DataBinding進行了數據的存儲和綁定。

最後代碼地址:

https://github.com/Hankkin/JetPack_Note

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章