以前的幾篇源碼分析咱們分別對
Navigation
、Lifecycles
、ViewModel
、LiveData
、進行了分析,也對JetPack有了更深刻的瞭解。可是Jetpack遠不止這些組件,今天的主角---Paging,Jetpack中的分頁組件,官方是這麼形容它的:‘’逐步從您的數據源按需加載信息‘’java
若是你對Jetpack組件有了解或者想對源碼有更深刻的瞭解,請看我以前的幾篇文章:android
1. Jetpack源碼解析---看完你就知道Navigation是什麼了?git
2. Jetpack源碼解析---Navigation爲何切換Fragment會重繪?github
3. Jetpack源碼解析---用Lifecycles管理生命週期數據庫
4. Jetpack源碼解析—LiveData的使用及工做原理後端
5. Jetpack源碼解析---ViewModel基本使用及源碼解析api
在個人Jetpack_Note系列中,對每一篇的分析都有相對應的代碼片斷及使用,我把它作成了一個APP,目前功能還不完善,代碼我也上傳到了GitHub上,參考了官方的Demo以及目前網上的一些文章,有興趣的小夥伴能夠看一下,別忘了給個Star。緩存
今天咱們的主角是Paging,介紹以前咱們先看一下效果:app
官方定義:
分頁庫Pagin Library是Jetpack的一部分,它能夠妥善的逐步加載數據,幫助您一次加載和顯示一部分數據,這樣的按需加載能夠減小網絡貸款和系統資源的使用。分頁庫支持加載有限以及無限的list,好比一個持續更新的信息源,分頁庫能夠與RecycleView無縫集合,它還能夠與LiveData或RxJava集成,觀察界面中的數據變化。
PageList是一個集合類,它以分塊的形式異步加載數據,每一塊咱們稱之爲頁。它繼承自AbstractList
,支持全部List的操做,它的內部有五個主要變量:
Config屬性:
PageList會經過DataSource加載數據,經過Config的配置,能夠設置一次加載的數量以及預加載的數量。除此以外,PageList還能夠想RecycleView.Adapter發送更新的信號,驅動UI的刷新。
DataSource<Key,Value> 顧名思義就是數據源,它是一個抽象類,其中Key
對應加載數據的條件信息,Value
對應加載數據的實體類。Paging庫中提供了三個子類來讓咱們在不一樣場景的狀況下使用:
PageListAdapter繼承自RecycleView.Adapter,和RecycleView實現方式同樣,當數據加載完畢時,通知RecycleView數據加載完畢,RecycleView填充數據;當數據發生變化時,PageListAdapter會接受到通知,交給委託類AsyncPagedListDiffer來處理,AsyncPagedListDiffer是對**DiffUtil.ItemCallback**持有對象的委託類,AsyncPagedListDiffer使用後臺線程來計算PagedList的改變,item是否改變,由DiffUtil.ItemCallback決定。
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
複製代碼
新建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)
}
}
複製代碼
上面咱們經過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
中設置了initialLoad
和netWorkState
的狀態值,同時經過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上。
咱們先看下官網給出的gif圖:
總結一下,Paging的基本原理爲:
基本原理在圖上咱們能夠很清晰的瞭解到了,本篇文章的Demo中結合了ViewModel以及DataBinding進行了數據的存儲和綁定。
最後代碼地址: