Jetpack新成員,Paging3從吐槽到真香 Paging 3簡介 上手Paging 3 在底部顯示加載狀態 最後

前陣子分享了一篇Jetpack的高質量文:深入解析Android Jetpack - 讓天下沒有難做的 App,大家表示不夠過癮,這裏就另外分享一篇,希望對大家的學習和工作有所幫助。
原文地址:guolin 大佬

各位小夥伴們大家早上好。

隨着Android 11的正式發佈,Jetpack家族也引入了許多新的成員。我之前有承諾過,對於新引入的App Startup、Hilt、Paging 3,我會分別寫一篇文章進行介紹。

現在,關於App Start和Hilt的文章我都已經寫完了,請參考 Jetpack新成員,App Startup一篇就懂 和 Jetpack新成員,一篇文章帶你玩轉Hilt和依賴注入 。

那麼本篇文章,我們要學習的自然就是Paging 3了。

Paging 3簡介

Paging是Google推出的一個應用於Android平臺的分頁加載庫。

事實上,Paging並不是現在纔剛剛推出的,而是之前就已經推出過兩個版本了。

但Paging 3和前面兩個版本的變化非常大,甚至可以說是完全不同的東西了。所以即使你之前沒有學習過Paging的用法也沒有關係,把Paging 3當成是一個全新的庫去學習就可以了。

我相信一定會有很多朋友在學習Paging 3的時候會產生和我相同的想法:本身Android上的分頁功能並不難實現,即使沒有Paging庫我們也完全做得出來,但爲什麼Paging 3要把一個本來還算簡單的功能設計得如此複雜呢?

是的,Paging 3很複雜,至少在你還不瞭解它的情況下就是如此。我在第一次學習Paging 3的時候就直接被勸退了,心想着何必用這玩意委屈自己呢,自己寫分頁功能又不是做不出來。

後來本着擁抱新技術的態度,我又去學習了一次Paging 3,這次算是把它基本掌握了,並且還在我的新開源項目 Glance 當中應用了Paging 3的技術。

如果現在再讓我來評價一下Paging 3,那麼我大概是經歷了一個由吐槽到真香的過程。理解了Paging 3之後,你會發現它提供了一套非常合理的分頁架構,我們只需要按照它提供的架構去編寫業務邏輯,就可以輕鬆實現分頁功能。我希望大家在看完這篇文章之後,也能覺得Paging 3香起來。

不過,本篇文章我不能保證它的易懂性。雖然很多朋友都覺得我寫的文章簡單易懂,但Paging 3的複雜性在於它關聯了太多其他的知識,如協程、Flow、MVVM、RecyclerView、DiffUtil等等,如果你不能將相關聯的這些知識都有所瞭解,那麼想要掌握Paging 3就會更有難度。

另外,由於Paging 3是Google基於Kotlin協程全新重寫的一個庫,所以它主要是應用於Kotlin語言(Java也能用,但是會更加複雜),並且以後這樣的庫會越來越多,比如Jetpack Compose等等。如果你對於Kotlin還不太瞭解的話,可以去參郭霖的新書《第一行代碼 Android 第3版》。

上手Paging 3

經過我自己的總結,我發現如果零散去介紹一些Paging 3的知識點是很難能掌握得了這個庫的。最好的學習方式就是直接上手,用Paging 3去做一個項目,項目做完了,你也基本就掌握了。本篇文章中我們就會採用這種方式來學習。

另外,我相信大家之前應該都做過分頁功能,正如我所說,這個功能並不難實現。但是現在,請你完全忘掉過去你所熟知的分頁方案,因爲它不僅對理解Paging 3沒有幫助,反而在很大程度上會影響你對Paging 3的理解。

是的,不要想着去監聽列表滑動事件,滑動到底部的時候發起一個網絡請求加載下一頁數據。Paging 3完全不是這麼用的,如果你還保留着這種過去的實現思路,在學習Paging 3的時候會很受阻。

那麼現在就讓我們開始吧。

首先新建一個Android項目,這裏我給它起名爲Paging3Sample。

接下來,我們在build.gradle的dependencies當中添加必要的依賴庫:

dependencies {
    ...
    implementation 'androidx.paging:paging-runtime:3.0.0-beta01'
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}

注意雖然我剛纔說,Paging 3是要和很多其他關聯庫結合到一起工作的,但是我們並不需要將這些關聯庫一一手動引入,引入了Paging 3之後,所有的關聯庫都會被自動下載下來。

另外這裏還引入了Retrofit的庫,因爲待會我們會從網絡上請求數據,並通過Paging 3進行分頁展示。

那麼在正式開始涉及Paging 3的用法之前,讓我們先來把網絡相關的代碼搭建好,方便爲Paging 3提供分頁數據。

這裏我準備採用GitHub的公開API來作爲我們這個項目的數據源,請注意GitHub在國內雖然一般都是可以訪問的,但有時接口並不穩定,如果你無法正常請求到數據的話,請自行科學上網。

我們可以嘗試在瀏覽器中請求如下接口地址:

https://api.github.com/search/repositories?sort=stars&q=Android&per_page=5&page=1

這個接口表示,會返回GitHub上所有Android相關的開源庫,以Star數量排序,每頁返回5條數據,當前請求的是第一頁。

服務器響應的數據如下,爲了方便閱讀,我對響應數據進行了簡化:

{
  "items": [
    {
      "id": 31792824,
      "name": "flutter",
      "description": "Flutter makes it easy and fast to build beautiful apps for mobile and beyond.",
      "stargazers_count": 112819,
    },
    {
      "id": 14098069,
      "name": "free-programming-books-zh_CN",
      "description": ":books: 免費的計算機編程類中文書籍,歡迎投稿",
      "stargazers_count": 76056,
    },
    {
      "id": 111583593,
      "name": "scrcpy",
      "description": "Display and control your Android device",
      "stargazers_count": 44713,
    },
    {
      "id": 12256376,
      "name": "ionic-framework",
      "description": "A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.",
      "stargazers_count": 43041,
    },
    {
      "id": 55076063,
      "name": "Awesome-Hacking",
      "description": "A collection of various awesome lists for hackers, pentesters and security researchers",
      "stargazers_count": 42876,
    }
  ]
}

簡化後的數據格式還是非常好理解的,items數組中記錄了第一頁包含了哪些庫,其中name表示該庫的名字,description表示該庫的描述,stargazers_count表示該庫的Star數量。

那麼下面我們就根據這個接口來編寫網絡相關的代碼吧,由於這部分都是屬於Retrofit的用法,我會介紹的比較簡略。

首先根據服務器響應的Json格式定義對應的實體類,新建一個Repo類,代碼如下所示:

data class Repo(
    @SerializedName("id") val id: Int,
    @SerializedName("name") val name: String,
    @SerializedName("description") val description: String?,
    @SerializedName("stargazers_count") val starCount: Int
)

然後定義一個RepoResponse類,以集合的形式包裹Repo類:

class RepoResponse(
    @SerializedName("items") val items: List<Repo> = emptyList()
)

接下來定義一個GitHubService用於提供網絡請求接口,如下所示:

interface GitHubService {

    @GET("search/repositories?sort=stars&q=Android")
    suspend fun searchRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): RepoResponse

    companion object {
        private const val BASE_URL = "https://api.github.com/"

        fun create(): GitHubService {
            return Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
                    .create(GitHubService::class.java)
        }
    }

}

這些都是Retrofit的標準用法,現在當調用searchRepos()函數時,Retrofit就會自動幫我們向GitHub的服務器接口發起一條網絡請求,並將響應的數據解析到RepoResponse對象當中。

好了,現在網絡相關的代碼都已經準備好了,下面我們就開始使用Paging 3來實現分頁加載功能。

Paging 3有幾個非常關鍵的核心組件,我們需要分別在這幾個核心組件中按部就班地實現分頁邏輯。

首先最重要的組件就是PagingSource,我們需要自定義一個子類去繼承PagingSource,然後重寫load()函數,並在這裏提供對應當前頁數的數據。

新建一個RepoPagingSource繼承自PagingSource,代碼如下所示:

class RepoPagingSource(private val gitHubService: GitHubService) : PagingSource<Int, Repo>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
        return try {
            val page = params.key ?: 1 // set page 1 as default
            val pageSize = params.loadSize
            val repoResponse = gitHubService.searchRepos(page, pageSize)
            val repoItems = repoResponse.items
            val prevKey = if (page > 1) page - 1 else null
            val nextKey = if (repoItems.isNotEmpty()) page + 1 else null
            LoadResult.Page(repoItems, prevKey, nextKey)
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Repo>): Int? = null

}

這段代碼並不長,但卻需要好好解釋一下。

在繼承PagingSource時需要聲明兩個泛型類型,第一個類型表示頁數的數據類型,我們沒有特殊需求,所以直接用整型就可以了。第二個類型表示每一項數據(注意不是每一頁)所對應的對象類型,這裏使用剛纔定義的Repo。

然後在load()函數當中,先通過params參數得到key,這個key就是代表着當前的頁數。注意key是可能爲null的,如果爲null的話,我們就默認將當前頁數設置爲第一頁。另外還可以通過params參數得到loadSize,表示每一頁包含多少條數據,這個數據的大小我們可以在稍後設置。

接下來調用剛纔在GitHubService中定義的searchRepos()接口,並把page和pageSize傳入,從服務器獲取當前頁所對應的數據。

最後需要調用LoadResult.Page()函數,構建一個LoadResult對象並返回。注意LoadResult.Page()函數接收3個參數,第一個參數傳入從響應數據解析出來的Repo列表即可,第二和第三個參數分別對應着上一頁和下一頁的頁數。針對於上一頁和下一頁,我們還額外做了個判斷,如果當前頁已經是第一頁或最後一頁,那麼它的上一頁或下一頁就爲null。

這樣load()函數的作用就已經解釋完了,可能你會發現,上述代碼還重寫了一個getRefreshKey()函數。這個函數是Paging 3.0.0-beta01版本新增的,以前的alpha版中並沒有。它是屬於Paging 3比較高級的用法,我們本篇文章涉及不到,所以直接返回null就可以了。

PagingSource相關的邏輯編寫完成之後,接下來需要創建一個Repository類。這是MVVM架構的一個重要組件,還不瞭解的朋友可以去參考《第一行代碼 Android 第3版》第15章的內容。

object Repository {

    private const val PAGE_SIZE = 50

    private val gitHubService = GitHubService.create()

    fun getPagingData(): Flow<PagingData<Repo>> {
        return Pager(
            config = PagingConfig(PAGE_SIZE),
            pagingSourceFactory = { RepoPagingSource(gitHubService) }
        ).flow
    }

}

這段代碼雖然很短,但是卻不易理解,因爲用到了協程的Flow。我無法在這裏展開解釋Flow是什麼,你可以簡單將它理解成協程中對標RxJava的一項技術。

當然這裏也沒有用到什麼複雜的Flow技術,正如你所見,上面的代碼很簡短,相比於理解,這更多是一種固定的寫法。

我們定義了一個getPagingData()函數,這個函數的返回值是Flow<PagingData<Repo>>,注意除了Repo部分是可以改的,其他部分都是固定的。

在getPagingData()函數當中,這裏創建了一個Pager對象,並調用.flow將它轉換成一個Flow對象。在創建Pager對象的時候,我們指定了PAGE_SIZE,也就是每頁所包含的數據量。又指定了pagingSourceFactory,並將我們自定義的RepoPagingSource傳入,這樣Paging 3就會用它來作爲用於分頁的數據源了。

將Repository編寫完成之後,我們還需要再定義一個ViewModel,因爲Activity是不可以直接和Repository交互的,要藉助ViewModel纔可以。新建一個MainViewModel類,代碼如下所示:

class MainViewModel : ViewModel() {

    fun getPagingData(): Flow<PagingData<Repo>> {
        return Repository.getPagingData().cachedIn(viewModelScope)
    }

}

代碼很簡單,就是調用了Repository中定義的getPagingData()函數而已。但是這裏又額外調用了一個cachedIn()函數,這是用於將服務器返回的數據在viewModelScope這個作用域內進行緩存,假如手機橫豎屏發生了旋轉導致Activity重新創建,Paging 3就可以直接讀取緩存中的數據,而不用重新發起網絡請求了。

寫到這裏,我們的這個項目已經完成了一大半了,接下來開始進行界面展示相關的工作。

由於Paging 3是必須和RecyclerView結合使用的,下面我們定義一個RecyclerView的子項佈局。新建repo_item.xml,代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:orientation="vertical">

    <TextView
        android:id="@+id/name_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:maxLines="1"
        android:ellipsize="end"
        android:textColor="#5194fd"
        android:textSize="20sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/description_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:maxLines="10"
        android:ellipsize="end" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:gravity="end"
        tools:ignore="UseCompoundDrawables">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_marginEnd="5dp"
            android:src="@drawable/ic_star"
            tools:ignore="ContentDescription" />

        <TextView
            android:id="@+id/star_count_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical" />

    </LinearLayout>

</LinearLayout>

這個佈局中使用到了一個圖片資源,可以到本項目的源碼中去獲取,源碼地址見文章最底部。

接下來定義RecyclerView的適配器,但是注意,這個適配器也比較特殊,必須繼承自PagingDataAdapter,代碼如下所示:

class RepoAdapter : PagingDataAdapter<Repo, RepoAdapter.ViewHolder>(COMPARATOR) {

    companion object {
        private val COMPARATOR = object : DiffUtil.ItemCallback<Repo>() {
            override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean {
                return oldItem == newItem
            }
        }
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val name: TextView = itemView.findViewById(R.id.name_text)
        val description: TextView = itemView.findViewById(R.id.description_text)
        val starCount: TextView = itemView.findViewById(R.id.star_count_text)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.repo_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val repo = getItem(position)
        if (repo != null) {
            holder.name.text = repo.name
            holder.description.text = repo.description
            holder.starCount.text = repo.starCount.toString()
        }
    }

}

相比於一個傳統的RecyclerView Adapter,這裏最特殊的地方就是要提供一個COMPARATOR。因爲Paging 3在內部會使用DiffUtil來管理數據變化,所以這個COMPARATOR是必須的。如果你以前用過DiffUtil的話,對此應該不會陌生。

除此之外,我們並不需要傳遞數據源給到父類,因爲數據源是由Paging 3在內部自己管理的。同時也不需要重寫getItemCount()函數了,原因也是相同的,有多少條數據Paging 3自己就能夠知道。

其他部分就和普通的RecyclerView Adapter沒什麼兩樣了,相信大家都能夠看得明白。

接下來就差最後一步了,讓我們把所有的一切都集成到Activity當中。

修改activity_main.xml佈局,在裏面定義一個RecyclerView和一個ProgressBar:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

</FrameLayout>

然後修改MainActivity中的代碼,如下所示:

class MainActivity : AppCompatActivity() {

    private val viewModel by lazy { ViewModelProvider(this).get(MainViewModel::class.java) }

    private val repoAdapter = RepoAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
        val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = repoAdapter
        lifecycleScope.launch {
            viewModel.getPagingData().collect { pagingData ->
                repoAdapter.submitData(pagingData)
            }
        }
        repoAdapter.addLoadStateListener {
            when (it.refresh) {
                is LoadState.NotLoading -> {
                    progressBar.visibility = View.INVISIBLE
                    recyclerView.visibility = View.VISIBLE
                }
                is LoadState.Loading -> {
                    progressBar.visibility = View.VISIBLE
                    recyclerView.visibility = View.INVISIBLE
                }
                is LoadState.Error -> {
                    val state = it.refresh as LoadState.Error
                    progressBar.visibility = View.INVISIBLE
                    Toast.makeText(this, "Load Error: ${state.error.message}", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

}

這裏最重要的一段代碼就是調用了RepoAdapter的submitData()函數。這個函數是觸發Paging 3分頁功能的核心,調用這個函數之後,Paging 3就開始工作了。

submitData()接收一個PagingData參數,這個參數我們需要調用ViewModel中返回的Flow對象的collect()函數才能獲取到,collect()函數有點類似於Rxjava中的subscribe()函數,總之就是訂閱了之後,消息就會源源不斷往這裏傳。

不過由於collect()函數是一個掛起函數,只有在協程作用域中才能調用它,因此這裏又調用了lifecycleScope.launch()函數來啓動一個協程。

其他地方應該就沒什麼需要解釋的了,都是一些傳統RecyclerView的用法,相信大家都能看得懂。

好了,這樣我們就把整個項目完成了,在正式運行項目之前,別忘了在你的AndroidManifest.xml文件中添加網絡權限:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.paging3sample">

    <uses-permission android:name="android.permission.INTERNET" />
    ...

</manifest>

現在運行一下程序,效果如下圖所示:

可以看到,GitHub上Android相關的開源庫已經成功顯示出來了。並且你可以不斷往下滑,Paging 3會自動加載更多的數據,彷彿讓你永遠也滑不到頭一樣。

如次一來,使用Paging 3來進行分頁加載的效果也就成功完成了。

總結一下,相比於傳統的分頁實現方案,Paging 3將一些瑣碎的細節進行了隱藏,比如你不需要監聽列表的滑動事件,也不需要知道知道何時應該加載下一頁的數據,這些都被Paging 3封裝掉了。我們只需要按照Paging 3搭建好的框架去編寫邏輯實現,告訴Paging 3如何去加載數據,其他的事情Paging 3都會幫我們自動完成。

在底部顯示加載狀態

根據Paging 3的設計,其實我們理論上是不應該在底部看到加載狀態的。因爲Paging 3會在列表還遠沒有滑動到底部的時候就提前加載更多的數據(這是默認屬性,可配置),從而產生一種好像永遠滑不到頭的感覺。

然而凡事總有意外,比如說當前的網速不太好,雖然Paging 3會提前加載下一頁的數據,但是當滑動到列表底部的時候,服務器響應的數據可能還沒有返回,這個時候就應該在底部顯示一個正在加載的狀態。

另外,如果網絡條件非常糟糕,還可能會出現加載失敗的情況,此時應該在列表底部顯示一個重試按鈕。

那麼接下來我們就來實現這個功能,從而讓項目變得更加完善。

創建一個footer_item.xml佈局,用於顯示加載進度條和重試按鈕:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">

    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

    <Button
        android:id="@+id/retry_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Retry" />

</FrameLayout>

然後創建一個FooterAdapter來作爲RecyclerView的底部適配器,注意它必須繼承自LoadStateAdapter,如下所示:

class FooterAdapter(val retry: () -> Unit) : LoadStateAdapter<FooterAdapter.ViewHolder>() {

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val progressBar: ProgressBar = itemView.findViewById(R.id.progress_bar)
        val retryButton: Button = itemView.findViewById(R.id.retry_button)
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.footer_item, parent, false)
        val holder = ViewHolder(view)
        holder.retryButton.setOnClickListener {
            retry()
        }
        return holder
    }

    override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
        holder.progressBar.isVisible = loadState is LoadState.Loading
        holder.retryButton.isVisible = loadState is LoadState.Error
    }

}

這仍然是一個非常簡單的Adapter,需要注意的地方大概只有兩點。

第一點,我們使用Kotlin的高階函數來給重試按鈕註冊點擊事件,這樣當點擊重試按鈕時,構造函數中傳入的函數類型參數就會被回調,我們待會將在那裏加入重試邏輯。

第二點,在onBindViewHolder()中會根據LoadState的狀態來決定如何顯示底部界面,如果是正在加載中那麼就顯示加載進度條,如果是加載失敗那麼就顯示重試按鈕。

最後,修改MainActivity中的代碼,將FooterAdapter集成到RepoAdapter當中:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        recyclerView.adapter = repoAdapter.withLoadStateFooter(FooterAdapter { repoAdapter.retry() })
        ...
    }

}

代碼非常簡單,只需要改動一行,調用RepoAdapter的withLoadStateFooter()函數即可將FooterAdapter集成到RepoAdapter當中。

另外注意這裏使用Lambda表達式來作爲傳遞給FooterAdapter的函數類型參數,在Lambda表示式中,調用RepoAdapter的retry()函數即可重新加載。

這樣我們就把底部顯示加載狀態的功能完成了,現在來測試一下吧,效果如下圖所示。

可以看到,首先我在設備上開啓了飛行模式,這樣當滑動到列表底部時就會顯示重試按鈕。

然後把飛行模式關閉,並點擊重試按鈕,這樣加載進度條就會顯示出來,並且成功加載出新的數據了。

最後

本文到這裏就結束了。

不得不說,我在文章中講解的這些知識點仍然只是Paging 3的基本用法,還有許多高級用法文中並沒有涵蓋。當然,這些基本用法也是最最常用的用法,所以如果你並不打算成爲Paging 3大師,掌握文中的這些知識點就已經足夠應對日常的開發工作了。

如果你還想要進一步進階學習Paging 3,可以參考Google官方的Codelab項目,地址是:
https://developer.android.com/codelabs/android-paging

我們剛纔一起編寫的Paging3Sample項目其實就是從Google官方的Codelab項目演化而來的,我根據自己的理解重寫了這個項目並進行了一定的簡化。直接學習原版項目,你將能學到更多的知識。

最後,如果你需要獲取Paging3Sample項目的源碼,請訪問以下地址:
https://github.com/guolindev/Paging3Sample

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