JetPack知識(shí)點(diǎn)實(shí)戰(zhàn)系列六:Paging實(shí)現(xiàn)加載更多和下拉刷新,錯(cuò)誤后重新請(qǐng)求

前面的教程我們遺留了一個(gè)問(wèn)題:我們的列表只能請(qǐng)求第一頁(yè),本節(jié)我們將實(shí)現(xiàn)分頁(yè)加載的效果和下拉刷新的效果。

本節(jié)內(nèi)容您將學(xué)習(xí)到如下內(nèi)容:

  1. 用Paging庫(kù)實(shí)現(xiàn)加載更多
  2. 用Paging庫(kù)和SwipeRefreshLayout結(jié)合實(shí)現(xiàn)下拉刷新
  3. 給RecyclerView添加Footer
  4. 加載失敗進(jìn)行重試
  5. Android幀動(dòng)畫(huà)的實(shí)現(xiàn)方式

Paging的優(yōu)勢(shì)

Paging庫(kù)之前,我們進(jìn)行分頁(yè)加載使用的方法是監(jiān)聽(tīng)RecyclerView的滾動(dòng)事件,當(dāng)快滾動(dòng)到底部的時(shí)候進(jìn)行新數(shù)據(jù)的請(qǐng)求。

這個(gè)方法有一定的問(wèn)題,譬如當(dāng)用戶(hù)在接近底部的時(shí)候快速上下移動(dòng),有可能會(huì)有多次請(qǐng)求發(fā)出,如果處理不當(dāng),就有可能漏掉數(shù)據(jù)或者產(chǎn)生重復(fù)數(shù)據(jù)。

Google引入的Paging,它抽象出來(lái)一些自動(dòng)加載的邏輯類(lèi),我們?cè)谶@里邏輯類(lèi)里面填入所需要的內(nèi)容,然后自動(dòng)分頁(yè)加載的過(guò)程就由Paging庫(kù)自動(dòng)給我們完成了。

Paging實(shí)現(xiàn)分頁(yè)加載更多

  • 首先需要引入Paging依賴(lài)庫(kù)
// 添加依賴(lài)
def paging_version = '2.1.2'
implementation "androidx.paging:paging-runtime:$paging_version"
  • 選擇合適的DataSource

DataSource就是數(shù)據(jù)源,顧名思義就是列表數(shù)據(jù)從這個(gè)類(lèi)里面獲取得到。

Paging提供有三種DataSource

  1. ItemKeyedDataSource - 使用場(chǎng)景:通過(guò)ID請(qǐng)求這個(gè)ID后面的數(shù)據(jù)
  2. PageKeyedDataSource - 使用場(chǎng)景:通過(guò)Page請(qǐng)求下一個(gè)Page的數(shù)據(jù)
  3. PositionalDataSource - 使用場(chǎng)景:請(qǐng)求從第X條到第Y條的數(shù)據(jù)

通過(guò)上面的介紹,我們已經(jīng)確定我們需要的是PageKeyedDataSource。

新建PlaylistDataSource繼承自PageKeyedDataSource

class PlaylistDataSource : PageKeyedDataSource<Int, PlayItem>() {
    override fun loadInitial(
        params: LoadInitialParams<Int>,
        callback: LoadInitialCallback<Int, PlayItem>
    ) {
        TODO("Not yet implemented")
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, PlayItem>) {
        TODO("Not yet implemented")
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, PlayItem>) {
        TODO("Not yet implemented")
    }
}

新建的這個(gè)類(lèi)構(gòu)造函數(shù)有一個(gè)泛型<Int, PlayItem>, Int是分頁(yè)的時(shí)候傳入的頁(yè)數(shù),PlayItem是每個(gè)Item對(duì)應(yīng)的數(shù)據(jù)模型。

初始化的時(shí)候需要復(fù)寫(xiě)三方方法:

  1. loadInitial是最開(kāi)始加載的時(shí)候調(diào)用的數(shù)據(jù)請(qǐng)求方法
  2. loadBefore是頁(yè)面向上滾動(dòng)的時(shí)候時(shí)候調(diào)用數(shù)據(jù)請(qǐng)求的方法
  3. loadAfter是頁(yè)面向上滾動(dòng)的時(shí)候時(shí)候調(diào)用的數(shù)據(jù)請(qǐng)求方法
  • 改造DataSource

我們知道了這三方復(fù)寫(xiě)方法的含義后,我們修改下代碼:

// 1
class PlaylistDataSource(private val type: String, private val scope: CoroutineScope) : PageKeyedDataSource<Int, PlayItem>() {

    override fun loadInitial(
        params: LoadInitialParams<Int>,
        callback: LoadInitialCallback<Int, PlayItem>
    ) {
        scope.launch {
            try {
                when (type) {
                    "推薦" -> {
                        // 2
                        val response = PlaylistRepository.getRecommendPlaylist(params.requestedLoadSize, 0)
                        // 3
                        callback.onResult(response.playlists, -1, 1)
                    }
                    "精品" -> {
                        val response = PlaylistRepository.getHighQualityPlaylist(params.requestedLoadSize, 0)
                        callback.onResult(response.playlists, -1, 1)
                    }
                    "官方" -> {
                        val response = PlaylistRepository.getOrgPlaylist(params.requestedLoadSize, 0)
                        callback.onResult(response.playlists, -1, 1)
                    }
                    else -> {
                        val response = PlaylistRepository.getPlaylistByCat(params.requestedLoadSize, 0, type)
                        callback.onResult(response.playlists, -1, 1)
                    }
                }
            } catch (e: Exception) {
                Log.d("PlaylistDataSource", "$e")
            }
        }
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, PlayItem>) {
        scope.launch {
            try {
                when (type) {
                    "推薦" -> {
                        // 4
                        val response = PlaylistRepository.getRecommendPlaylist(params.requestedLoadSize, params.key)
                        // 5
                        callback.onResult(response.playlists, params.key + 1)
                    }
                    "精品" -> {
                        val response = PlaylistRepository.getHighQualityPlaylist(params.requestedLoadSize, params.key)
                        callback.onResult(response.playlists, params.key + 1)
                    }
                    "官方" -> {
                        val response = PlaylistRepository.getOrgPlaylist(params.requestedLoadSize, params.key)
                        callback.onResult(response.playlists, params.key + 1)
                    }
                    else -> {
                        val response = PlaylistRepository.getPlaylistByCat(params.requestedLoadSize, params.key, type)
                        callback.onResult(response.playlists, params.key + 1)
                    }
                }
            } catch (e: Exception) {
                Log.d("PlaylistDataSource", "$e")
            }
        }
    }

    // 6
    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, PlayItem>) {
        TODO("Not yet implemented")
    }

}

一步步解釋下代碼的含義:

  1. 構(gòu)造函數(shù)添加了兩個(gè)變量typescope,分別為歌單類(lèi)型和協(xié)程作用域
  2. params.requestedLoadSizePlaylistDataSource初始化的時(shí)候配置的,代表每頁(yè)請(qǐng)求多少個(gè)Item,項(xiàng)目中配置的是15。如何配置后續(xù)介紹。
  3. callback.onResult 是通過(guò)回調(diào)將結(jié)果返回,這個(gè)方法有三個(gè)參數(shù),第一個(gè)參數(shù)response.playlists是數(shù)據(jù)結(jié)果,第二個(gè)參數(shù)-1是請(qǐng)求上一頁(yè)需要傳入的頁(yè)的數(shù)值(我們的項(xiàng)目中這個(gè)值沒(méi)有實(shí)際意義),第三個(gè)參數(shù)1是請(qǐng)求下一頁(yè)需要傳入的頁(yè)的數(shù)值

提示:1 這個(gè)數(shù)值會(huì)通過(guò)Paging傳給loadAfter方法中的params: LoadParams<Int>這個(gè)參數(shù)

  1. params.requestedLoadSize和步驟2中的意義相同, params.key就是上面callback.onResult傳的值1
  2. callback.onResult(response.playlists, params.key + 1) 中的params.key + 1就是將頁(yè)面數(shù)值設(shè)置成當(dāng)前的數(shù)值+1
  3. loadBefore我們用不到,所以可以不用覆寫(xiě)方法
  • DataSource.Factory

DataSource一般由DataSource.Factory來(lái)初始化。

class PlaylistDataSourceFactory(private val type: String, private val scope: CoroutineScope) : DataSource.Factory<Int, PlayItem>() {

    override fun create(): DataSource<Int, PlayItem> {
        return PlaylistDataSource(type, scope)
    }

}

DataSource.Factory 需要覆寫(xiě)create方法,返回一個(gè)DataSource對(duì)象就可以了。

  • 改造AdapterPagedListAdapter

使用Paging功能需要將PlaylistItemAdapter繼承由ListAdapter改為PagedListAdapter。這樣就可以了,因?yàn)?strong>PagedListAdapter中實(shí)現(xiàn)了對(duì)Paging的支持。

class PlaylistItemAdapter:
    PagedListAdapter<PlayItem, PlaylistItemAdapter.PlaylistItemHolder>(DiffCallback) {
    ...
    }

問(wèn)題:可否不使用DataSource.Factory來(lái)創(chuàng)建DataSource對(duì)象?

  • 修改PlayListViewModel

由于網(wǎng)絡(luò)請(qǐng)求移到了DataSourceViewModel的代碼就大大精簡(jiǎn)了。只留下一個(gè)變量。

class PlayListViewModel(private val type: String) : ViewModel() {

    var pagedlistLiveData = LivePagedListBuilder<Int, PlayItem>(
        PlaylistDataSourceFactory(type, viewModelScope),
        PagedList.Config.Builder().setPageSize(15).build()
    ).build()
}

這段代碼比較長(zhǎng),我們分布解釋下:

  1. LivePagedListBuilder有兩個(gè)參數(shù),第一個(gè)參數(shù)就是DataSource對(duì)象,這里是通過(guò)上面創(chuàng)建的工廠(chǎng)方法創(chuàng)建的。這里要求傳入的是DataSource.Factory
  2. PagedList.Config.Builder().setPageSize(15).build() 這個(gè)setPageSize(15)代表的是每頁(yè)請(qǐng)求15條數(shù)據(jù)。當(dāng)然PagedList.Config還可以進(jìn)行其他一些配置。
  3. LivePagedListBuilder通過(guò)build方法返回的是一個(gè)LiveData
public LiveData<PagedList<Value>> build() {
    ...
}

問(wèn)題1:什么是PagedList

PagedList是一個(gè)改造后的List,當(dāng)用戶(hù)滑動(dòng)列表接近底部的時(shí)候就會(huì)委托DataSource去請(qǐng)求新的數(shù)據(jù)。

問(wèn)題2:為什么需要用LiveData包裝PagedList?

首先LiveData包裝PagedList可以使其能被觀察,這樣就能實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)UI的重繪;

再次,用戶(hù)進(jìn)行下拉刷新的時(shí)候通過(guò)只需要調(diào)用invalidate方法,****LiveData****會(huì)重新生成一個(gè)新的PagedList,這個(gè)PagedList會(huì)委托DataSource去請(qǐng)求新的數(shù)據(jù) 這樣所有的流程就又可以重新開(kāi)始自動(dòng)進(jìn)行了。

  • 修改Fragment
var viewModel = ViewModelProviders.of(this, object : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(PlayListViewModel::class.java)) {
            return PlayListViewModel(it)  as T
        }
        throw  IllegalArgumentException(" unKnown ViewModel class ")
    }
}).get(PlayListViewModel::class.java)

由于需要初始ViewModel的時(shí)候需要傳參,這里修改了ViewModel的初始化方法,重寫(xiě)ViewModelProvider.Factorycreate方法。

監(jiān)聽(tīng)LiveData

viewModel.pagedlistLiveData.observe(viewLifecycleOwner, Observer {
    playAdapter.submitList(it)
})

到目前為止,加載更多的功能就實(shí)現(xiàn)了。

效果圖

Paging和SwipRefreshLayout組合實(shí)現(xiàn)下拉刷新

  • 實(shí)現(xiàn)下來(lái)刷新需要修改下布局,將根布局設(shè)置成SwipeRefreshLayout
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/refreshlayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".Fragment.PlayListFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/constraint_root"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
  • 添加重新開(kāi)始請(qǐng)求的方法
/* 下拉刷新 */
fun resetQuery() {
    pagedlistLiveData.value?.dataSource?.invalidate()
}

這個(gè)方法在前面有解釋?zhuān)筒辉儋樖隽恕?/p>

  • 下拉監(jiān)聽(tīng)
viewModel.pagedlistLiveData.observe(viewLifecycleOwner, Observer {
    playAdapter.submitList(it)
    // 1
    refreshlayout.isRefreshing = false
})

// 2
refreshlayout.setOnRefreshListener {
    viewModel.resetQuery()
}

  1. 刷新完成后,isRefreshing置為false, 這時(shí)候刷新動(dòng)畫(huà)會(huì)取消
  2. 監(jiān)聽(tīng)下拉執(zhí)行刷新
效果圖

給RecyclerView添加加載狀態(tài)的Footer

細(xì)心的你可能會(huì)發(fā)現(xiàn)當(dāng)RecyclerView滑到底部的時(shí)候可以實(shí)現(xiàn)自動(dòng)加載更多,但是會(huì)有小小的卡頓,特別是網(wǎng)絡(luò)不太好的時(shí)候,因?yàn)榫W(wǎng)絡(luò)請(qǐng)求是需要加載時(shí)間的。

為了良好的用戶(hù)體驗(yàn),可以加載過(guò)程中需要添加一個(gè)Footer,給用戶(hù)一個(gè)正在加載的反饋。此外也可以通過(guò)修改Footer的文案,當(dāng)加載出現(xiàn)錯(cuò)誤或者所有數(shù)據(jù)都加載完后給用戶(hù)一個(gè)提示。

示例如下:

加載中
加載完成
點(diǎn)擊重試

點(diǎn)擊重試可以重新加載請(qǐng)求失敗的頁(yè)的數(shù)據(jù)

  • 首先建一個(gè)Footer而布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:gravity="center">

    <ImageView
        android:id="@+id/loading_iv"
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:layout_marginEnd="10dp"
        android:layout_weight="0"
        android:background="@drawable/loading_list" />

    <TextView
        android:id="@+id/loading_tv"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_weight="0"
        android:gravity="center_vertical"
        android:text="加載中..."
        android:textColor="#9E9E9E"
        android:textSize="16sp" />
</LinearLayout>

footer比較簡(jiǎn)單,就是有一個(gè)圖片loading_iv和文本loading_tv

  • 由于這個(gè)Footer是放在Recyclervie中,所以需要建立一個(gè)LoadingViewHolder
class LoadingViewHolder(v: View) : RecyclerView.ViewHolder(v) {

    companion object {
        // 1
        fun instance(parent: ViewGroup): LoadingViewHolder {
            val v = LayoutInflater.from(parent.context).inflate(R.layout.loading_layout, parent, false)
            return LoadingViewHolder(v)
        }
    }

    // 2
    fun bindNetWorkStatus(loadingStatus: LoadingStatus?) {
        // 3
        when(loadingStatus) {
            LoadingStatus.Failed -> {
                itemView.loading_tv.text = "點(diǎn)擊重試"
                itemView.loading_iv.visibility = View.GONE
                itemView.isClickable = true
            }
            LoadingStatus.Completed -> {
                itemView.loading_tv.text = "加載完畢"
                itemView.loading_iv.visibility = View.GONE
                itemView.isClickable = false
            }
            LoadingStatus.Loading -> {
                itemView.loading_tv.text = "加載中..."
                itemView.loading_iv.visibility = View.VISIBLE
                itemView.isClickable = false
            }
        }
    }
}

代碼解釋如下:

  1. 創(chuàng)建了一個(gè)類(lèi)方法instance,加載布局文件,初始化LoadingViewHolder
  2. bindNetWorkStatus根據(jù)不同的LoadingStatus展示不同的樣式, LoadingStatus.Failed時(shí)候可以點(diǎn)擊重試

加載狀態(tài)的枚舉定義如下:

// 加載的狀態(tài)
enum class LoadingStatus {
    InitalLoading, // 初次加載
    Loading,       // 正在加載
    Failed,        // 加載失敗
    Completed      // 數(shù)據(jù)全部加載完
}
  • 改造Adapter
先定義一個(gè)是否顯示Footer的變量并且添加覆寫(xiě)兩個(gè)方法:
class PlaylistItemAdapter(private val viewModel: PlayListViewModel):
    PagedListAdapter<PlayItem, RecyclerView.ViewHolder>(DiffCallback) {

    // 1
    private var hasLoadingFooter = false

    // 2
    override fun getItemCount(): Int {
        return super.getItemCount() + if (hasLoadingFooter) 1 else 0
    }
    
    // 3
    override fun getItemViewType(position: Int): Int {
        return if (hasLoadingFooter && position == itemCount - 1) R.layout.loading_layout else R.layout.item_playlist
    }

}

代碼解釋如下:

  1. 定義一個(gè)hasLoadingFooter的變量控制是否顯示Footer,第一次加載的時(shí)候不顯示Footer,因?yàn)槲覀円呀?jīng)有下拉刷新了。
  2. getItemCount是返回顯示多少I(mǎi)tem,hasLoadingFooter為真的時(shí)候得比Item多加一行,
  3. getItemViewType是返回每個(gè)Item對(duì)應(yīng)的布局文件, 因?yàn)椴煌腎tem顯示的樣式不一樣,需要通過(guò)這個(gè)方法指定
修改兩個(gè)覆寫(xiě)方法
override fun onCreateViewHolder(
    parent: ViewGroup,
    viewType: Int
): RecyclerView.ViewHolder {
    return when(viewType) {
        R.layout.item_playlist -> {
            PlaylistItemHolder.instance(parent)
        }
        else -> {
            LoadingViewHolder.instance(parent)
        }
    }
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    when(holder.itemViewType) {
        R.layout.loading_layout -> {
            (holder as LoadingViewHolder).bindNetWorkStatus(_loadingStatus)
        }
        else -> {
            getItem(position)?.let {
                (holder as PlaylistItemHolder).bindPlayItem(it)
            }
        }
    }
}
  1. onCreateViewHolder根據(jù)不同的viewType返回不同的ViewHolder
  2. onBindViewHolder根據(jù)不同的itemViewType進(jìn)行不同的綁定

目前為止Adapter準(zhǔn)備好了,也就是說(shuō)UI層面的邏輯好了,那現(xiàn)在就需要有一個(gè)加載狀態(tài)的觸發(fā)了。很明顯加載狀態(tài)觸發(fā)的位置是DataSource。

疑問(wèn): DataSourceViewModel持有,如何反向傳遞數(shù)據(jù)呢? 上節(jié)有介紹可以有CallBackLiveData等形式。

采取LiveData反向傳遞如何實(shí)現(xiàn)呢?

  • 實(shí)現(xiàn)Datasource回傳LoadingStatusViewModel

實(shí)現(xiàn)邏輯是ViewModel 定義一個(gè)LiveData,層層傳遞給Datasource。
Datasource持有這個(gè)LiveData,就可以修改值了。

<!-- PlayListViewModel -->
class PlayListViewModel(type: String) : ViewModel() {

    // 1
    var loadingStatusLiveData: LiveData<LoadingStatus> = _loadingStatusLiveData

    // 2
    var pagedListLiveData = LivePagedListBuilder<Int, PlayItem>(
        PlaylistDataSourceFactory(type, viewModelScope, _loadingStatusLiveData),
        PagedList.Config.Builder().setPageSize(15).build()
    ).build()

}

<!-- PlaylistDataSourceFactory -->
class PlaylistDataSourceFactory(private val type: String, private val scope: CoroutineScope, private val loadingStatusLiveData: MutableLiveData<LoadingStatus>) : DataSource.Factory<Int, PlayItem>() {

    override fun create(): DataSource<Int, PlayItem> {
        return PlaylistDataSource(type, scope, loadingStatusLiveData)
    }

}

<!-- PlaylistDataSource -->
// 1
class PlaylistDataSource(private val type: String, private val scope: CoroutineScope, private val loadingStatusLiveData: MutableLiveData<LoadingStatus>) : PageKeyedDataSource<Int, PlayItem>() {

    override fun loadInitial(
        params: LoadInitialParams<Int>,
        callback: LoadInitialCallback<Int, PlayItem>
    ) {
        // 2
        loadingStatusLiveData.postValue(LoadingStatus.InitalLoading)
        scope.launch {
            try {
                ...
            } catch (e: Exception) {
                // 2
                loadingStatusLiveData.postValue(LoadingStatus.Failed)
            }
        }
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, PlayItem>) {
        // 2
        loadingStatusLiveData.postValue(LoadingStatus.Loading)
        scope.launch {
            try {
                ...
            } catch (e: Exception) {
                // 2
                loadingStatusLiveData.postValue(LoadingStatus.Failed)
            }
        }
    }

}

  • Adapter監(jiān)聽(tīng)LoadingStatus的變化
<!-- PlayListFragment -->
viewModel.loadingStatusLiveData.observe(viewLifecycleOwner, Observer {
    playAdapter.updateLoadingStatus(it)
})

<!-- PlaylistItemAdapter -->
// 更新加載狀態(tài)
fun updateLoadingStatus(loadingStatus: LoadingStatus) {
    _loadingStatus = loadingStatus
    if (loadingStatus == LoadingStatus.InitalLoading) {
        hideLoading()
    } else {
        showLoading()
    }
}

private fun hideLoading() {
    if (hasLoadingFooter) {
        notifyItemRemoved(itemCount - 1)
    }
    hasLoadingFooter = false
}

private fun showLoading() {
    if (hasLoadingFooter) {
        notifyItemChanged(itemCount - 1)
    } else {
        hasLoadingFooter = true
        notifyItemInserted(itemCount - 1)
    }
}

這幾個(gè)方法的意義比較簡(jiǎn)單,就是LoadingStatus改變后刷新RecyclerView,及Footer的顯示和隱藏。

一個(gè)小的功能寫(xiě)了不少代碼,主要是流程比較的長(zhǎng),但是由于分層,邏輯確是很清晰。

流程圖
遺留問(wèn)題,由于GridLayoutManager,是每行三列,所以Footer也只有三分之一寬度。需要改成全屏,覆寫(xiě)onAttachedToRecyclerView方法:
// 這個(gè)方法解決Footer 全屏
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
    super.onAttachedToRecyclerView(recyclerView)
    var layoutManager:RecyclerView.LayoutManager = recyclerView.layoutManager!!
    if (layoutManager is GridLayoutManager) {
        layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup(){
            override fun getSpanSize(position: Int): Int {
                return if (getItemViewType(position) == R.layout.loading_layout) {
                    layoutManager.spanCount  // Footer時(shí)返回三個(gè)的單元格,從而占據(jù)整個(gè)一行的寬度
                } else {
                    1  // 正常情況下返回一個(gè)單元格
                }
            }
        }
    }
}

發(fā)生網(wǎng)絡(luò)錯(cuò)誤后重試

  • 用一個(gè)函數(shù)隊(duì)形保留錯(cuò)誤現(xiàn)場(chǎng)
public var retryFun: (() -> Any)? = null

override fun loadInitial(
    params: LoadInitialParams<Int>,
    callback: LoadInitialCallback<Int, PlayItem>
) {
    ...
    // 1
    retryFun = null
    scope.launch {
        try {
        } catch (e: Exception) {
            ...
            // 2
            retryFun = {loadInitial(params, callback)}
        }
    }
}

override fun loadAfter(params: LoadParams<Int>, callback:LoadCallback<Int, PlayItem>) {
    // 1
    retryFun = null
    scope.launch {
        try {
            ...
        } catch (e: Exception) {
            ...
            // 2
            retryFun = { loadAfter(params, callback) }
        }
    }
}

這段代碼的意思是:
定義retryFun變量,如果發(fā)生錯(cuò)誤就把調(diào)用的方法和參數(shù)賦值給retryFun記錄下來(lái)。

  • ViewModel中定義retryFun函數(shù)
// 重新嘗試
fun retry() {
    (pagedListLiveData.value?.dataSource as PlaylistDataSource).let {
        it.retryFun?.invoke()
    }
}
  • Adapter中調(diào)用retryFun函數(shù)
LoadingViewHolder.instance(parent).also {
    it.itemView.setOnClickListener {
        viewModel.retry()
    }
}

AdapterViewModel是獨(dú)立的,所以可以把ViewModel傳入Adapter。

PlaylistItemAdapter(private val viewModel: PlayListViewModel)

這樣整個(gè)流程也就完成了。

重試流程圖

最后的效果如下所示:

整體效果

幀動(dòng)畫(huà)

Footer有一個(gè)幀動(dòng)畫(huà),由于我在本機(jī)網(wǎng)絡(luò)加載較快,所以可能不太明顯。效果如

加載中

接下來(lái)我們就實(shí)現(xiàn)下這個(gè)幀動(dòng)畫(huà)的效果

  • Drawable文件中加入四個(gè)圖片,這四個(gè)圖片將用來(lái)輪流顯示
    icn_loading1,icn_loading2,icn_loading3,icn_loading4

  • Drawable文件創(chuàng)建一個(gè)loading_list.xml文件, 代碼如下

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item android:drawable="@drawable/icn_loading1" android:duration="150" />
    <item android:drawable="@drawable/icn_loading2" android:duration="150" />
    <item android:drawable="@drawable/icn_loading3" android:duration="150" />
    <item android:drawable="@drawable/icn_loading4" android:duration="150" />
</animation-list>
  • 將這個(gè)Drawable文件作為ImageView的背景
<ImageView
    android:id="@+id/loading_iv"
    ...
    android:background="@drawable/loading_list" />
  • 代碼中開(kāi)始動(dòng)畫(huà)和結(jié)束動(dòng)畫(huà)
private fun startAnimation() {
    val drawable = itemView.loading_iv.background as? AnimationDrawable

    drawable?.let {
        if (!it.isRunning) it.start()
    }
}

fun stopAnimation() {
    val drawable = itemView.loading_iv.background as? AnimationDrawable
    drawable?.let {
        if (it.isRunning) it.stop()
    }
}

通過(guò)這幾步,這個(gè)加載動(dòng)畫(huà)就實(shí)現(xiàn)了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容