即學(xué)即用Android Jetpack - Paging 3

技術(shù)不止,文章有料,加 JiuXinDev 入群,Android 搬磚路上不孤單

前言

又到了學(xué)習(xí) Android Jetpack 的時(shí)間了,之前我已經(jīng)寫過了一篇《即學(xué)即用Android Jetpack - Paging》,不過Android Jetpack 一直都在更新,這不,Paging 2 已經(jīng)升級(jí)到了 Paging 3,那么,大家可能有很多要關(guān)注的問題,比如:

  • Paging 3 到底升級(jí)了那些東西?
  • 如何使用最新的 Paging 3?

這是我本期要和大家討論的東西。本期的最終效果:

效果圖

如果想學(xué)習(xí) Android Jetpack,可以參考我之前的文章《學(xué)習(xí)Android Jetpack? 實(shí)戰(zhàn)和教程這里全都有!》,這已經(jīng)是 《即學(xué)即用Android Jetpack》系列的第八篇啦~

本篇的源碼地址:【Hoo】

目錄

目錄.png

一、Paging 2 和 Paging 3 有什么不同?

如果你沒有使用過 Paging 2,那么你可以跳過本章節(jié)(友情提醒~)。

如果你使用過 Paging 2,你會(huì)發(fā)現(xiàn) Paging 3 簡(jiǎn)直是大刀闊斧,很多 API 的使用方式都變了,簡(jiǎn)單說一下主要改變的東西:

  1. 支持 Kotlin 中的 Flow。
  2. 簡(jiǎn)化數(shù)據(jù)源 PagingSource 的實(shí)現(xiàn)。
  3. 增加請(qǐng)求數(shù)據(jù)時(shí)狀態(tài)的回調(diào),支持設(shè)置 Header 和 Footer。
  4. 支持多種方式請(qǐng)求數(shù)據(jù),比如網(wǎng)絡(luò)請(qǐng)求和數(shù)據(jù)庫請(qǐng)求。

二、介紹

友情提示
官方文檔:Paging 3
谷歌實(shí)驗(yàn)室:官方教程
官方Demo:Github倉庫查詢Demo

1. 定義

看一下官方的定義:

The Paging library helps you load and display pages of data from a larger dataset from local storage or over network.

就是幫助你采用分頁的方式解決顯示包含本地?cái)?shù)據(jù)庫和網(wǎng)絡(luò)的大數(shù)據(jù)集的問題。

2. 優(yōu)點(diǎn)

Paging 3 的優(yōu)點(diǎn):

  • 使用內(nèi)存幫你緩存數(shù)據(jù)。
  • 內(nèi)置請(qǐng)求去重,幫助你更有效率的顯示數(shù)據(jù)。
  • 自動(dòng)發(fā)起請(qǐng)求當(dāng) RecyclerView 滑到底部的時(shí)候。
  • 支持 Kotlin 中的 協(xié)程Flow,還有 LiveDataRxJava2。
  • 內(nèi)置狀態(tài)處理,包括刷新、錯(cuò)誤、加載等狀態(tài)。

3. 幾個(gè)重要的類

先看一下結(jié)構(gòu):

Paging3架構(gòu).png

里面幾個(gè)類的作用:

  • PagingSource:?jiǎn)我坏臄?shù)據(jù)源。
  • RemoteMediator:其實(shí) RemoteMediator 也是單一的數(shù)據(jù)源,它會(huì)在 PagingSource 沒有數(shù)據(jù)的時(shí)候,再使用 RemoteMediator 提供的數(shù)據(jù),如果既存在數(shù)據(jù)庫請(qǐng)求,又存在網(wǎng)絡(luò)請(qǐng)求,通常 PagingSource 用于進(jìn)行數(shù)據(jù)庫請(qǐng)求,RemoteMediator 進(jìn)行網(wǎng)絡(luò)請(qǐng)求。
  • PagingData:?jiǎn)未畏猪摂?shù)據(jù)的容器。
  • Pager:用來構(gòu)建 Flow<PagingData> 的類,實(shí)現(xiàn)數(shù)據(jù)加載完成的回調(diào)。
  • PagingDataAdapter:分頁加載數(shù)據(jù)的 RecyclerView 的適配器。

簡(jiǎn)述一下就是 PagingSourceRemoteMediator 充當(dāng)數(shù)據(jù)源的角色,ViewModel 使用 Pager 中提供的 Flow<PagingData> 監(jiān)聽數(shù)據(jù)刷新,每當(dāng) RecyclerView 即將滾動(dòng)到底部的時(shí)候,就會(huì)有新的數(shù)據(jù)的到來,最后,PagingAdapter 復(fù)雜展示數(shù)據(jù)。

三、實(shí)戰(zhàn)

為了簡(jiǎn)單起見,先從單一的數(shù)據(jù)源開始。

第一步 引入依賴

dependencies {
  def paging_version = "3.0.0-alpha08"

  implementation "androidx.paging:paging-runtime:$paging_version"

  // 用于測(cè)試
  testImplementation "androidx.paging:paging-common:$paging_version"

  // [可選] RxJava 支持
  implementation "androidx.paging:paging-rxjava2:$paging_version"

  // ... 其他配置不重要,具體可以查看官方文檔
}

第二步 配置數(shù)據(jù)源

Paging 2 中,里面有三種 Page Source,開發(fā)者使用的時(shí)候,還得去思考對(duì)應(yīng)的場(chǎng)景,想的腦瓜子疼。

Paging 3 中,如果只有單一的數(shù)據(jù)源,那么你要做的很簡(jiǎn)單,繼承 PagingSource 即可。

private const val SHOE_START_INDEX = 0;

class CustomPageDataSource(private val shoeRepository: ShoeRepository) : PagingSource<Int, Shoe>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Shoe> {
        val pos = params.key ?: SHOE_START_INDEX
        val startIndex = pos * params.loadSize + 1
        val endIndex = (pos + 1) * params.loadSize
        return try {
            // 從數(shù)據(jù)庫拉去數(shù)據(jù)
            val shoes = shoeRepository.getPageShoes(startIndex.toLong(), endIndex.toLong())
            // 返回你的分頁結(jié)果,并填入前一頁的 key 和后一頁的 key
            LoadResult.Page(
                shoes,
                if (pos <= SHOE_START_INDEX) null else pos - 1,
                if (shoes.isNullOrEmpty()) null else pos + 1
            )
        }catch (e:Exception){
            LoadResult.Error(e)
        }
        
    }
}

正常用 LoadResult.Page() 返回結(jié)果,出現(xiàn)錯(cuò)誤的情況用 LoadResult.Error 返回錯(cuò)誤。

第三步 生成可觀察的數(shù)據(jù)集

這里可觀察數(shù)據(jù)集包括 LiveData 、Flow 以及 RxJava 中的 ObservableFlowable,其中,RxJava 需要單獨(dú)引入擴(kuò)展庫去支持的。

這里的可觀察數(shù)據(jù)集 shoes 使用的是 Flow:

class ShoeModel constructor(private val shoeRepository: ShoeRepository) : ViewModel() {

    /**
     * @param config 分頁的參數(shù)
     * @param pagingSourceFactory 單一數(shù)據(jù)源的工廠,在閉包中提供一個(gè)PageSource即可
     * @param remoteMediator 同時(shí)支持網(wǎng)絡(luò)請(qǐng)求和數(shù)據(jù)庫請(qǐng)求的數(shù)據(jù)源
     * @param initialKey 初始化使用的key
     */
    var shoes = Pager(config = PagingConfig(
        pageSize = 20
        , enablePlaceholders = false
        , initialLoadSize = 20
    ), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).flow

    // ... 省略
}

可以看到,我們中了的 shoes: Flow<PagingData<Shoe>> 是由 Pager().flow 提供的。Pager 中的參數(shù)稍微解釋一下:

  • PagerConfig 可以提供分頁的參數(shù),比如每一頁的加載數(shù)、初始加載數(shù)和最大數(shù)等。
  • pagingSourceFactoryremoteMediator 都是數(shù)據(jù)源,我們使用其中的一個(gè)即可。

第四步 創(chuàng)建Adapter

和普通的 Adapter 沒有特別大的區(qū)別,主要是:

  • 繼承 PagingDataAdapter
  • 提供 DiffUtil.ItemCallback<Shoe>

實(shí)際代碼:

/**
 * 鞋子的適配器 配合Data Binding使用
 */
class ShoeAdapter constructor(val context: Context) :
    PagingDataAdapter<Shoe, ShoeAdapter.ViewHolder>(ShoeDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            ShoeRecyclerItemBinding.inflate(
                LayoutInflater.from(parent.context)
                , parent
                , false
            )
        )
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val shoe = getItem(position)
        holder.apply {
            bind(onCreateListener(shoe!!.id), shoe)
            itemView.tag = shoe
        }
    }

    /**
     * Holder的點(diǎn)擊事件
     */
    private fun onCreateListener(id: Long): View.OnClickListener {
        return View.OnClickListener {
            val intent = Intent(context, DetailActivity::class.java)
            intent.putExtra(BaseConstant.DETAIL_SHOE_ID, id)
            context.startActivity(intent)
        }
    }


    class ViewHolder(private val binding: ShoeRecyclerItemBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(listener: View.OnClickListener, item: Shoe) {
            binding.apply {
                this.listener = listener
                this.shoe = item
                executePendingBindings()
            }
        }
    }
}

class ShoeDiffCallback: DiffUtil.ItemCallback<Shoe>() {
    override fun areItemsTheSame(oldItem: Shoe, newItem: Shoe): Boolean {
        return oldItem.id == newItem.id
    }

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

第五步 在UI中使用

先來簡(jiǎn)單一點(diǎn)的,如果只顯示數(shù)據(jù),我們要做的是:

  1. 創(chuàng)建和設(shè)置適配器。
  2. 開啟一個(gè)協(xié)程
  3. 在協(xié)程中接收 Flow 提供的數(shù)據(jù)。

我的代碼:

    private fun onSubscribeUi(binding: ShoeFragmentBinding) {
        binding.lifecycleOwner = this

        // 初始化RecyclerView部分
        val adapter = ShoeAdapter(context!!)
        binding.recyclerView.adapter = adapter
        job = viewModel.viewModelScope.launch(Dispatchers.IO) {
            viewModel.shoes.collect() {
                adapter.submitData(it)
            }
        }
        // ... 省略
    }

第六步 設(shè)置Header和Footer

顯然 Paging 3 還有更多的功能,對(duì)!它還支持添加 HeaderFooter,官方示例是把它們用作上拉刷新和下拉加載更多的控件。

官方實(shí)例

不過 Hoo 中并沒有使用 HeaderFooter,我們來看看官方怎么使用的:

  1. 創(chuàng)建一個(gè) HeaderAdapter or FooterAdapter 繼承自 LoadStateAdapter,跟普通 Adapter 不一樣的地方在于它在 onBindViewHolder 方法中提供了 LoadState 參數(shù),它可以提供當(dāng)前 PagingLoading、NotLoadingError 的狀態(tài)。
  2. 跟普通 Adapter 一樣創(chuàng)建自己需要的 ViewHolder
  3. 稍微修改一下第五步中設(shè)置 ShoeAdapter,調(diào)用它的withLoadStateHeaderAndFooter 方法綁定 HeaderFooter 的適配器:
    private fun onSubscribeUi(binding: ShoeFragmentBinding) {
        binding.lifecycleOwner = this

        // 初始化RecyclerView部分
        val adapter = ShoeAdapter(context!!).withLoadStateHeaderAndFooter(
            header = ReposLoadStateAdapter { adapter.retry() },
            footer = ReposLoadStateAdapter { adapter.retry() }
        )
        binding.recyclerView.adapter = adapter
        // ... 省略
    }

需要說明一下,我這里使用的偽代碼。想要看詳細(xì)的教程,可以官網(wǎng)的這一步教程。

第七步 監(jiān)聽數(shù)據(jù)加載的狀態(tài)

PagingDataAdapter 除了添加 HeaderFooter,還可以監(jiān)聽數(shù)據(jù)的加載狀態(tài),狀態(tài)對(duì)應(yīng)的類是 LoadState,它有三種狀態(tài):

  1. Loading:數(shù)據(jù)加載中。
  2. NotLoading:內(nèi)存中有已經(jīng)獲取的數(shù)據(jù),即使往下滑,Paging 也不需要請(qǐng)求更多的數(shù)據(jù)。
  3. Error:請(qǐng)求數(shù)據(jù)時(shí)返回了一個(gè)錯(cuò)誤。

監(jiān)聽數(shù)據(jù)狀態(tài)的代碼:

adapter.addLoadStateListener {state:CombinedLoadStates->
    //... 狀態(tài)監(jiān)聽
}

監(jiān)聽方法就是這么簡(jiǎn)單,可以看到這個(gè) state 并不是 LoadState,而是一個(gè) CombinedLoadStates,顧名思義,就是多個(gè) LoadState 組合而成的狀態(tài)類,它里面有:

  1. refresh:LoadState:刷新時(shí)的狀態(tài),因?yàn)榭梢哉{(diào)用 PagingDataAdapter#refresh() 方法進(jìn)行數(shù)據(jù)刷新。
  2. append:LoadState:可以理解為 RecyclerView 向下滑時(shí)數(shù)據(jù)的請(qǐng)求狀態(tài)。
  3. prepend:LoadState:可以理解為RecyclerView 向上滑時(shí)數(shù)據(jù)的請(qǐng)求狀態(tài)。
  4. sourcemediator 分別包含上面123的屬性,source 代表單一的數(shù)據(jù)源,mediator 代表多數(shù)據(jù)源的場(chǎng)景,sourcemediator 二選一。

解釋了這么多,說一下我的玩法,下拉刷新我使用了第三方的刷新控件 SmartRefreshLayout,這就意味著我要自己處理 SmartRefreshLayout 的加載狀態(tài)。布局文件不貼了,就是 SmartRefreshLayout + RecyclerView + FloatingActionButton。

成果圖(Gif有一點(diǎn)點(diǎn)問題):

刷新效果

使用代碼就是:

    private fun onSubscribeUi(binding: ShoeFragmentBinding) {
        binding.lifecycleOwner = this

        // 初始化RecyclerView部分
        val adapter = ShoeAdapter(context!!)
        // 數(shù)據(jù)加載狀態(tài)的回調(diào)
        adapter.addLoadStateListener { state:CombinedLoadStates ->
            currentStates = state.source
            // 如果append沒有處于加載狀態(tài),但是refreshLayout出于加載狀態(tài),refreshLayout停止加載狀態(tài)
            if (state.append is LoadState.NotLoading && binding.refreshLayout.isLoading) {
                refreshLayout.finishLoadMore()
            }
            // 如果refresh沒有出于加載狀態(tài),但是refreshLayout出于刷新狀態(tài),refreshLayout停止刷新
            if (state.source.refresh is LoadState.NotLoading && binding.refreshLayout.isRefreshing) {
                refreshLayout.finishRefresh()
            }
        }
        binding.recyclerView.adapter = adapter
        binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)

                val lastPos = getLastVisiblePosition(binding.recyclerView)
                if (!(lastPos == adapter.itemCount - 1 && currentStates?.append is LoadState.Loading)) {
                    binding.refreshLayout.finishLoadMore()
                }
            }
        })
        job = viewModel.viewModelScope.launch(Dispatchers.IO) {
            viewModel.shoes.collect {
                adapter.submitData(it)
            }
        }
        binding.refreshLayout.setRefreshHeader(DropBoxHeader(context))
        binding.refreshLayout.setRefreshFooter(ClassicsFooter(context))
        binding.refreshLayout.setOnLoadMoreListener {
            // 如果當(dāng)前數(shù)據(jù)已經(jīng)全部加載完,就不再加載
            if(currentStates?.append?.endOfPaginationReached == true)
                binding.refreshLayout.finishLoadMoreWithNoMoreData()
        }

       //... 省略無關(guān)代碼
    }

到這兒,正常的流程就走通了。

四、更多

1. 使用RemoteMediator

RemoteMediator 的使用機(jī)制跟 PageSource 沒什么區(qū)別,默認(rèn)是先走 PageSource 的數(shù)據(jù),如果 PageSource 提供的數(shù)據(jù)返回為空的情況,才會(huì)走 RemoteMediator 的數(shù)據(jù),所以你可以用:

  • PageSource:進(jìn)行數(shù)據(jù)庫請(qǐng)求提供數(shù)據(jù)。
  • RemoteMediator:進(jìn)行網(wǎng)絡(luò)請(qǐng)求,然后對(duì)請(qǐng)求成功的數(shù)據(jù)進(jìn)行數(shù)據(jù)庫的存儲(chǔ)。

鑒于使用 RemoteMediator 跟普通的方式?jīng)]什么本質(zhì)區(qū)別,我這里就不再做更多介紹,感興趣的同學(xué)可以查看文檔。

2. 談?wù)勎矣龅降囊恍┛?/h4>
界面

之前點(diǎn)擊品牌按鈕, Paing 2 對(duì)應(yīng)的版本使用的 LiveData,這是我之前的寫法:

class ShoeModel constructor(shoeRepository: ShoeRepository) : ViewModel() {
    // 品牌的觀察對(duì)象 默認(rèn)觀察所有的品牌
    private val brand = MutableLiveData<String>().apply {
        value = ALL
    }

    // 鞋子集合的觀察類
    val shoes: LiveData<PagedList<Shoe>> = brand.switchMap {
        // Room數(shù)據(jù)庫查詢,只要知道返回的是LiveData<List<Shoe>>即可
        if (it == ALL) {
            // LivePagedListBuilder<Int,Shoe>( shoeRepository.getAllShoes(),PagedList.Config.Builder()
            LivePagedListBuilder<Int, Shoe>(
                CustomPageDataSourceFactory(shoeRepository) // DataSourceFactory
                , PagedList.Config.Builder()
                    .setPageSize(10) // 分頁加載的數(shù)量
                    .setEnablePlaceholders(false) // 當(dāng)item為null是否使用PlaceHolder展示
                    .setInitialLoadSizeHint(10) // 預(yù)加載的數(shù)量
                    .build()
            )
                .build()
            //shoeRepository.getAllShoes()
        } else {
            val array: Array<String> =
                when (it) {
                    NIKE -> arrayOf("Nike", "Air Jordan")
                    ADIDAS -> arrayOf("Adidas")
                    else -> arrayOf(
                        "Converse", "UA"
                        , "ANTA"
                    )
                }
            shoeRepository.getShoesByBrand(array)
                .createPagerList(6, 6)
        }
    }

    fun setBrand(brand: String) {
        this.brand.value = brand
    }

    companion object {
        public const val ALL = "所有"

        public const val NIKE = "Nike"
        public const val ADIDAS = "Adidas"
        public const val OTHER = "other"
    }
}

切換到 Paging 3 的時(shí)候遇到一些坑:

先把可觀察的數(shù)據(jù)集從 Flow 轉(zhuǎn)到 LiveData,也就是從 Pager(...).flowPager(...).livedata:

class ShoeModel constructor(private val shoeRepository: ShoeRepository) : ViewModel() {

    /**
     * @param config 分頁的參數(shù)
     * @param pagingSourceFactory 單一數(shù)據(jù)源的工廠,在閉包中提供一個(gè)PageSource即可
     * @param remoteMediator 同時(shí)支持網(wǎng)絡(luò)請(qǐng)求和數(shù)據(jù)庫請(qǐng)求的數(shù)據(jù)源
     * @param initialKey 初始化使用的key
     */
    var shoes = Pager(config = PagingConfig(
        pageSize = 20
        , enablePlaceholders = false
        , initialLoadSize = 20
    ), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).liveData

    //....
}

緊接著,我在 ShoeFragment 進(jìn)行數(shù)據(jù)的監(jiān)聽:

job = viewModel.viewModelScope.launch() {
    viewModel.shoes.observe(viewLifecycleOwner, Observer<PagingData<Shoe>> {
        adapter.submitData(viewLifecycleOwner.lifecycle,it)
    })
}

第一個(gè)坑出現(xiàn)了,在 Paging 2 中,PagingSouce 獲取數(shù)據(jù)是處在子線程的,而 Paging 3 中,獲取數(shù)據(jù)處在當(dāng)前線程,不會(huì)做線程切換,如果直接使用數(shù)據(jù)庫查詢,會(huì)報(bào)錯(cuò),因?yàn)?Room 中規(guī)定,數(shù)據(jù)庫查詢不能在主線程。

好家伙,既然主線程不可以請(qǐng)求數(shù)據(jù)庫,那么我就把它放在IO線程,在 launch 方法中加上 Dispatchers.IO

job = viewModel.viewModelScope.launch(Dispatchers.IO) {
    viewModel.shoes.observe(viewLifecycleOwner, Observer<PagingData<Shoe>> {
        adapter.submitData(viewLifecycleOwner.lifecycle,it)
    })
}

第二個(gè)坑安排上,LiveData#observe(...) 方法不能發(fā)生在后臺(tái)線程。

這就沒辦法了,只能使用 Flow 去實(shí)現(xiàn),不過 Flow 也有坑,當(dāng)數(shù)據(jù)源發(fā)生變化的時(shí)候,數(shù)據(jù)需要重新監(jiān)聽,代碼也沒有了之前的優(yōu)雅:

class ShoeModel constructor(private val shoeRepository: ShoeRepository) : ViewModel() {

    var shoes = Pager(config = PagingConfig(
        pageSize = 20
        , enablePlaceholders = false
        , initialLoadSize = 20
    ), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).flow

    fun setBrand(br: String) {
        if (br == ALL) {
            shoes = Pager(config = PagingConfig(
                pageSize = 20
                , enablePlaceholders = false
                , initialLoadSize = 20
            ), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).flow
        } else {
            val array: Array<String> =
                when (br) {
                    NIKE -> arrayOf("Nike", "Air Jordan")
                    ADIDAS -> arrayOf("Adidas")
                    else -> arrayOf(
                        "Converse", "UA"
                        , "ANTA"
                    )
                }
            shoes = shoeRepository.getShoesByBrand(array).createPager(20, 20).flow
        }
    }

    //....
}

class ShoeFragment : Fragment() {
    // ... 省略

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // ... 省略
        onSubscribeUi(binding)
        return binding.root
    }

    /**
     * 鞋子數(shù)據(jù)更新的通知
     */
    private fun onSubscribeUi(binding: ShoeFragmentBinding) {
        // ... 省略
        mNike.setOnClickListener {
            viewModel.setBrand(ShoeModel.NIKE)
            reInitSubscribe(adapter)
            shoeAnimation()
        }

        mAdi.setOnClickListener {
            viewModel.setBrand(ShoeModel.ADIDAS)
            reInitSubscribe(adapter)
            shoeAnimation()
        }

        mOther.setOnClickListener {
            viewModel.setBrand(ShoeModel.OTHER)
            reInitSubscribe(adapter)
            shoeAnimation()
        }
    }

    private fun reInitSubscribe(adapter: ShoeAdapter) {
        job?.cancel()
        job = viewModel.viewModelScope.launch(Dispatchers.IO) {
            viewModel.shoes.collect() {
                adapter.submitData(it)
            }
        }
    }

    private fun setViewVisible(isShow: Boolean) {
        nikeGroup.visibility = if (isShow) View.VISIBLE else View.GONE
        adiGroup.visibility = if (isShow) View.VISIBLE else View.GONE
        otherGroup.visibility = if (isShow) View.VISIBLE else View.GONE
    }   
}

期間也試了 LiveData 轉(zhuǎn) Flow 的操作符,也沒起作用,可能自己研究的不深,有了解的同學(xué)可以探討一下哈~

五、總結(jié)

相比較 Paging 2 而言,Paging 3 的功能確實(shí)更加健全了,也值得去使用。

在后續(xù)的文章中,我還將和大家探討一下 Paging 3 的源碼,當(dāng)然,Paging 3 現(xiàn)在還處于 Alpha 階段,并不適用于我們的生產(chǎn)環(huán)境,所以我還打算和大家聊聊如何更好的使用 Paging 2。

表情包
最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 Android 列表分頁加載組件 paging3 alpha版本已經(jīng)出來很久了。目前到了alpha7;分享一...
    JarryLeo閱讀 18,146評(píng)論 13 14
  • 前言 即學(xué)即用Android Jetpack系列Blog的目的是通過學(xué)習(xí)Android Jetpack完成一個(gè)簡(jiǎn)單...
    九心_閱讀 14,182評(píng)論 6 32
  • 上個(gè)周末晚上看到了鴻洋大神的公眾號(hào)推送文章<<Jetpack重磅更新>>,于是乎點(diǎn)開文章看了一下具體內(nèi)容,在翻閱的...
    Colaman丶閱讀 1,490評(píng)論 0 2
  • 漸變的面目拼圖要我怎么拼? 我是疲乏了還是投降了? 不是不允許自己墜落, 我沒有滴水不進(jìn)的保護(hù)膜。 就是害怕變得面...
    悶熱當(dāng)乘涼閱讀 4,462評(píng)論 0 13
  • 感覺自己有點(diǎn)神經(jīng)衰弱,總是覺得手機(jī)響了;屋外有人走過;每次媽媽不聲不響的進(jìn)房間突然跟我說話,我都會(huì)被嚇得半死!一整...
    章魚的擁抱閱讀 2,364評(píng)論 4 5

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