Jetpack 源碼分析(五) - Paging3源碼分析(上)

??Google爸爸在今年(2020年)的Jetpack庫里面更新paging組件怕午,推出了Paing3获列。按照Google爸爸文檔的描述,Paing3完全使用的是kotlin似枕,其中還包括了kotlin 的很多特性溯警,比如說協(xié)程趣苏,F(xiàn)low和Channel等狡相。出于好奇梯轻,想要了解其使用方式和內(nèi)部實現(xiàn)原理,因此尽棕,便寫下這篇文章喳挑。
??其實,Paging3的推出時間對于我來說挺尷尬滔悉,Paging3推出的第一個版本時伊诵,那個時候我在看Paging2的源碼。當(dāng)時在學(xué)習(xí)Paging2的時候回官,內(nèi)心一直在想曹宴,我還有必要學(xué)習(xí)這個嗎?不得不說歉提,Google爸爸推陳出新的速度太快了笛坦,都來不及追隨了。
??好了苔巨,不廢話了版扩,開始本文的主題吧。本文打算由淺入深的分析Paging3侄泽,從最開始的基本使用講到內(nèi)部實現(xiàn)原理礁芦。主要內(nèi)容如下:

  1. 基本使用。包括PagingSource的使用悼尾,RemoteMediator的使用柿扣。
  2. 基本架構(gòu)。Paging3內(nèi)部定義了很多的類闺魏,在分析原理之前窄刘,我們需要理解清楚每個類的含義,以及類之間是怎么串聯(lián)起來舷胜。這些理解清楚了娩践,看代碼的時候才不會疑惑活翩。
  3. 首次加載的過程。主要是分析Paging怎么實現(xiàn)第一次Refresh操作的翻伺。
  4. 加載更多的過程材泄。主要是分析Pagin怎么去加載上一頁或者下一頁的數(shù)據(jù)。
  5. RemoteMediator的實現(xiàn)原理吨岭。了解Paging的同學(xué)應(yīng)該都知道拉宗,RemoteMediator主要是將網(wǎng)絡(luò)上的數(shù)據(jù)放在緩存在數(shù)據(jù)庫里,以保證無網(wǎng)的狀態(tài)下也能加載數(shù)據(jù)辣辫。由于RemoteMediator的實現(xiàn)原理跟普通單一數(shù)據(jù)源加載方式有很大不同的旦事,所以我單獨拎出來分析。(下篇內(nèi)容)

??同時介于篇幅的原因急灭,將本文的內(nèi)容拆分上下兩篇姐浮,本文是上篇內(nèi)容,下篇內(nèi)容主要講解RemoteMediator葬馋。
??在閱讀本文之前卖鲤,默認(rèn)大家已經(jīng)掌握如下的知識,本文不做單獨介紹:

  1. Kotlin 的協(xié)程畴嘶,F(xiàn)low和Channel蛋逾。
  2. Room的基本使用。

??本文參考資料:

  1. Paging 3 library overview
  2. Load and display paged data
  3. Page from network and database
  4. Transform data streams
  5. 使用 Paging 3 實現(xiàn)分頁加載

??注意窗悯,本文Paging源碼均來自于3.0.0-alpha08版本区匣。

1. 概述

??Paging組件本身的作用本文就不做過多的介紹,有興趣的同學(xué)可以看看:Jetpack 源碼分析(四) - Paging源碼分析蒋院。在這里亏钩,我介紹一下Paging3和Paging2的不同。

  1. Adapter從PagedListAadpter更換成為了PagingDataAdapter悦污。
  2. 廢棄了PagedList铸屉,內(nèi)部使用PagingDataPagePresenter來存儲。網(wǎng)上有人說PagingData替代了PagedList切端,我覺得不太準(zhǔn)確彻坛。因為在Paging2中,PagedList只會創(chuàng)建一次踏枣,那就是在Refresh的時候昌屉,同時PagedList還兼有數(shù)據(jù)存儲和處理,存儲了已加載所有的數(shù)據(jù)茵瀑;而在Paging3里面间驮,PagingData只存儲一次Refresh + 多次Prepend + 多次Append的數(shù)據(jù),之所以這么說马昨,因為在Paging3中竞帽,即使不手動Refresh扛施,也會可能會Refresh,這個主要是跟RemoteMediator有關(guān)屹篓。所以在Paging3中疙渣,PagingData只是用來提交數(shù)據(jù),而不是存儲數(shù)據(jù)的堆巧,存儲和處理數(shù)據(jù)主要是由PagePresenter妄荔。
  3. 簡化了數(shù)據(jù)加載的邏輯。在Paging2中谍肤,我們通過自定義DataSource來實現(xiàn)加載啦租,定義的時候需要考慮初始化加載,以及更多加載的區(qū)別荒揣,同時還需要實現(xiàn)不同DataSource篷角,來區(qū)分不同場景的數(shù)據(jù);而在Paging3中乳附,只需要定義PagingSource負(fù)責(zé)加載數(shù)據(jù)即可内地。注意伴澄,我們不能理解為DataSource已經(jīng)過時了赋除,一是Google爸爸并沒有把這個類標(biāo)記為過時,其次在RemoteMediator內(nèi)部還在使用它非凌。

??Paging3內(nèi)部實現(xiàn)完全使用Kotlin實現(xiàn)举农,其中使用Flow和Channel替代了LiveData,通過協(xié)程實現(xiàn)異步處理敞嗡。作為使用者颁糟,我喜歡這種設(shè)計,因為更加簡潔喉悴;但是棱貌,做了開發(fā)者和源碼閱讀者來說,增加很多的工作量箕肃。從兩個方面來說:

  1. 使用Flow和Channel實現(xiàn)監(jiān)聽婚脱。Flow的上游來源在下游是不可知的,如果下游出現(xiàn)問題勺像,沒法追溯障贸,只能愣看代碼。
  2. 使用協(xié)程進(jìn)行異步處理吟宦。debug難度增加篮洁,就目前而言,debug Paging3內(nèi)部的源碼存在兩個問題殃姓,要么打了debug的斷點根本不生效袁波,要么就是雖然生效了瓦阐,但是始終斷不下來。

??例如篷牌,打了debug的斷點根本不生效垄分。我想看一下PageFetcherSnapshotdoLoad方法調(diào)用情況,發(fā)現(xiàn)打了斷點根本不生效:


??在另外一個地方打了斷點娃磺,雖然生效了薄湿,但是根本斷不下來:

??大家看上面動圖,我在前后打了兩個斷點偷卧,前面那個斷點不能斷豺瘤,后面那個斷點可以斷下來。真的听诸,看Paging3的源碼真的太難了坐求,連普通的debug都成問題。
??不過晌梨,抱怨歸抱怨桥嗤,源碼我們還是要看的。

2. 基本使用

??在分析原理實現(xiàn)之前仔蝌,我們先來看看Paging3的基本使用泛领。Paging的使用主要分為兩個部分:單一數(shù)據(jù)源和多級數(shù)據(jù)源。

(1). 單一數(shù)據(jù)源

??在Paging3中敛惊,PagingSource就是負(fù)責(zé)單一數(shù)據(jù)源的加載渊鞋。那么什么是單一數(shù)據(jù)源呢?我的理解是瞧挤,只有一個來源的數(shù)據(jù)就是單一數(shù)據(jù)源锡宋,比如說我從PagingSource中獲取數(shù)據(jù),只能來自于一個地方特恬,要么網(wǎng)絡(luò)执俩,要么本地,這個就是所謂單一數(shù)據(jù)源癌刽。我們來看看役首,是怎么使用的吧,主要分為三步:

  1. 定義一個PagingSource妒穴,并且使用ViewModel 將提供給Activity/Fragment宋税。
  2. 自定義一個RecyclerView的Adapter,繼承于PagingDataAdapter
  3. Activity/Fragment從ViewModel獲取一個Flow,并且監(jiān)聽讼油,同時將監(jiān)聽的到內(nèi)容通過Adapter的submitData方法提交給它杰赛。

??我們來看看具體的代碼實現(xiàn)。

A. 定義PagingSource

??我們直接看代碼:

class CustomPagingSource : PagingSource<Int, Message>() {
    private var count = 1
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Message> {
        return try {
            val message = Service.create().getMessage(params.pageSize)
            LoadResult.Page(message, null, count++)
        } catch (e: Exception) {
            e.printStackTrace()
            LoadResult.Error(e)
        }
    }
}

??相比于Paging2(Paging2里面是DataSource)矮台,Paging3對PagingSource的定義就變得非常簡單了乏屯,只需要我們實現(xiàn)一個方法就行了根时,不過這里面我們需要注意兩個點:

  1. load方法的返回值是一個LoadResult對象。其中如果請求成功的話辰晕,需要返回LoadResult.Page;如果請求失敗蛤迎,需要返回LoadResult.Error對象,這一點大家一定要注意含友。在這里替裆,我特意的分析一下LoadResult.Page內(nèi)部幾個參數(shù)含義,其內(nèi)部幾個參數(shù)含義如下:
參數(shù) 含義
data 加載的數(shù)據(jù)列表窘问。
prevKey 上一頁數(shù)據(jù)的key辆童。如果為空的話,表示沒有上一頁數(shù)據(jù)惠赫。
nextKey 下一頁數(shù)據(jù)的key把鉴。如果為空的話,表示沒有下一頁數(shù)據(jù)儿咱。
itemsBefore 當(dāng)前加載完畢頁之前需要加載占位符的數(shù)量庭砍,默認(rèn)值是COUNT_UNDEFINED
,表示不會創(chuàng)建占位符混埠。比如說怠缸,當(dāng)前加載的是第一頁的數(shù)據(jù),itemsBefore
為100的話岔冀,那么在這頁數(shù)據(jù)之前凯旭,會有100個占位符概耻,這個我們也可以從
Adapter的itemCount驗證使套,這里就不展示。同時鞠柄,這里也不解釋什么是
占位符侦高,感興趣的同學(xué)可以參考:Jetpack 源碼分析(四) - Paging源碼分析
itemsAfter itemsBefore,表示當(dāng)前加載完畢頁之后需要加載占位符的數(shù)量厌杜。
  1. 在加載數(shù)據(jù)完成之后返回LoadResult.Page奉呛,注意設(shè)置prevKeynextKey的值,否則有可能不會自動加載下一頁的數(shù)據(jù)夯尽。
B. 定義ViewModel并且暴露Flow

??在Paging3中瞧壮,Google爸爸推薦使用Flow替代LiveData,用來給UI層傳遞數(shù)據(jù)匙握。這里為了簡單起見咆槽,我們就不獨樹一幟,按照Google爸爸推薦來定義圈纺∏胤蓿看看代碼:

class NetWorkViewModel : ViewModel() {
    val messageFlow = Pager(PagingConfig(20)) {
        CustomPagingSource()
    }.flow.cachedIn(viewModelScope)
}

??如上便是ViewModel定義的完整過程麦射,非常的簡單,不過這里我們需要注意如下:

  1. Flow是從Pager里面拿到的灯谣,其中它有幾個參數(shù)潜秋,我們需要注意一下,如下:
參數(shù) 含義
config Paging 的配置項胎许,這個配置項我們在Paging2里面已經(jīng)解釋過了
峻呛,這里就不過多的解釋,只解釋新增幾個配置參數(shù)辜窑。initialLoadSize
杀饵,表示初始化加載需要加載的數(shù)據(jù)量,默認(rèn)是pageSize * 3,針對于這
個參數(shù)我們特別要跟pageSize區(qū)分開來谬擦,雖然我們設(shè)置了pageSize鳍置,
但是如果沒有設(shè)置initialLoadSize床未,第一次加載的數(shù)據(jù)量是pageSize * 3
jumpThreshold,根據(jù)Google爸爸注釋和實現(xiàn)的代碼兔综,大概的意思是,當(dāng)滑
動到了邊界锄蹂,并且加載的數(shù)據(jù)量超過了設(shè)置的閾值了袁,那么就觸發(fā)refresh邏
輯,此字段我會在分析RemoteMediator里面分析贺氓。
initialKey 初始加載的key蔚叨。
remoteMediator 多級數(shù)據(jù)源請求的工具類,這個類的意思后續(xù)我會專門的介紹辙培,
這里先不介紹了蔑水。
pagingSourceFactory 用來創(chuàng)建一個PagingSource。
  1. 我們將flow對象扬蕊,通過cachedIn方法緩存在一個viewModelScope里面搀别。根據(jù)Google爸爸的介紹,這樣可以在Activity重建的時候尾抑,F(xiàn)low里面已經(jīng)轉(zhuǎn)換過的數(shù)據(jù)不會再次轉(zhuǎn)換歇父,而是直接拿來用。

??如上便是ViewModel的定義再愈,是不是很簡單了呢榜苫?接下來,我們再來看看View層是怎么監(jiān)聽的翎冲。

C. View層監(jiān)聽Flow

??首先View層要想監(jiān)聽Flow的回調(diào)垂睬,RecyclerView 的Adapter必須繼承于PagingDataAdapter。關(guān)于Adapter的定義,這里就不單獨的講解羔飞,相信大家都非常的熟悉肺樟。
??我們直接來看代碼:

        lifecycleScope.launchWhenCreated {
            mMessageFlow.collectLatest {
                adapter.submitData(it)
            }
        }

??上面展示的是Adapter監(jiān)聽Flow的數(shù)據(jù)變化,其中逻淌,我們需要注意如下的內(nèi)容:

  1. collectLatest方法是掛起函數(shù)么伯,所以必須在協(xié)程里面調(diào)用。lifecycleScope是Google爸爸給我們提供生命周期敏感的協(xié)程作用卡儒,保證頁面銷毀時協(xié)程能正常取消田柔。

(2).多級數(shù)據(jù)源

??單一數(shù)據(jù)源比較簡單,數(shù)據(jù)來源只有一個地方骨望。實際上硬爆,在真實項目開發(fā)過程中,數(shù)據(jù)來源并沒有那么簡單擎鸠,很常見的場景是:本地數(shù)據(jù)庫 + 網(wǎng)絡(luò)數(shù)據(jù)缀磕。如果手機沒有網(wǎng)絡(luò),可以使用本地數(shù)據(jù)庫顯示數(shù)據(jù)劣光,以保證用戶在無網(wǎng)絡(luò)的時候能正常使用App袜蚕。
??出于這種情況的考慮,Google爸爸在設(shè)計Paging3時绢涡,新增加了一個工具類RemoteMediator,用來實現(xiàn)多級數(shù)據(jù)源牲剃。我們來看看具體代碼實現(xiàn)(如下代碼涉及到Room庫,這里就不單獨解釋雄可,默認(rèn)大家都懂)凿傅。
??首先使用Room定義一個Dao,用以對數(shù)據(jù)庫操作数苫,通常來說Dao里面必須含有三個操作

  1. 查詢聪舒,需要返回一個PagingSource對象。
  2. 插入文判,當(dāng)有新數(shù)據(jù)过椎,我們需要同步到數(shù)據(jù)庫中去,以備后用戏仓。
  3. 清空所有數(shù)據(jù),當(dāng)用戶刷新操作成功之后亡鼠,應(yīng)該清空之前所有的數(shù)據(jù)赏殃,然后才插入新的數(shù)據(jù)。
@Dao
interface MessageDao {

    @Query("select * from message")
    fun getMessage(): PagingSource<Int, Message>

    @Insert(
        onConflict = OnConflictStrategy.REPLACE
    )
    fun insertMessage(messages: List<Message>)

    @Query("delete from message")
    fun clearMessage()
}

??Dao的定義便是如上间涵,大家需要注意的是仁热,查詢操作返回的是一個PagingSource對象。如果大家發(fā)現(xiàn)Room生成的代碼不支持PagingSource類型,可以將Room升級到2.3.0版本抗蠢。
??其次就是實現(xiàn)RemoteMediator接口举哟,用以從網(wǎng)絡(luò)獲取數(shù)據(jù),我們直接來看具體的實現(xiàn):

class CustomRemoteMediator : RemoteMediator<Int, Message>() {

    private val mMessageDao = DataBaseHelper.dataBase.messageDao()
    private var count = 0

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, Message>
    ): MediatorResult {
        val startIndex = when (loadType) {
            LoadType.REFRESH -> 0
            LoadType.PREPEND -> return MediatorResult.Success(true)
            LoadType.APPEND -> {
                val stringBuilder = StringBuilder()
                state.pages.forEach {
                    stringBuilder.append("size = ${it.data.size}, count = ${it.data.count()}\n")
                }
                Log.i("pby123", stringBuilder.toString())
                count += 20
                count
            }
        }
        val messages = Service.create().getMessage(20, startIndex)
        DataBaseHelper.dataBase.withTransaction {
            Log.i("pby123", "loadType = $loadType")
            if (loadType == LoadType.REFRESH) {
                mMessageDao.clearMessage()
            }
            mMessageDao.insertMessage(messages)
        }
        return MediatorResult.Success(messages.isEmpty())
    }
}

??RemoteMediator的定義跟PagingSource比較類似迅矛,都有l(wèi)oad方法妨猩,只不過他們所做的事情不太一樣,RemoteMediator的load方法做了兩件事:

  1. 通過不同的loadType獲取key秽褒,這個key可能是prevKey壶硅,也可能是nextKey。
  2. 通過拿到的key從網(wǎng)絡(luò)上請求數(shù)據(jù)销斟,并且放到數(shù)據(jù)庫中去庐椒。需要注意的是,這里并并沒有將請求的數(shù)據(jù)結(jié)果通過result返回去蚂踊,那么是因為RemoteMediator的職責(zé)是從網(wǎng)絡(luò)上請求數(shù)據(jù)约谈,然后放到數(shù)據(jù)庫里面,這一點跟PagingSource有很大的不同犁钟。那么RemoteMediator請求回來時怎么提交給到Adapter呢窗宇?這個我們在后續(xù)的內(nèi)容會重點分析。

??最后就是ViewModel里面的定義特纤,我們直接來看代碼:

class NetWorkAndDataBaseViewModel : ViewModel() {
    @ExperimentalPagingApi
    val messageFlow = Pager(PagingConfig(20), remoteMediator = CustomRemoteMediator()) {
        DataBaseHelper.dataBase.messageDao().getMessage()
    }.flow.cachedIn(viewModelScope)
}

??多級數(shù)據(jù)源跟單一數(shù)據(jù)源關(guān)于Flow的創(chuàng)建不太一樣军俊,主要體現(xiàn)在如下兩點:

  1. Pager的構(gòu)造方法里面需要傳入一個RemoteMediator對象,這個就是我們自定義的RemoteMediator捧存。
  2. 其次粪躬,PagingSource不需要我們自定義,直接從我們之前定義的Dao里面獲取就行昔穴。Room會通過代碼生成的方式镰官,返回一個指定的PagingSource對象。

??關(guān)于多級數(shù)據(jù)源的其地方都跟單一數(shù)據(jù)源都是一樣的吗货,這里就不再贅述了泳唠。

(3). 代碼

??為了大家方便理解,我將我的Demo代碼上傳到github:KotlinDemo宙搬。同時額外的說一句笨腥,后續(xù)我會將所有的Demo代碼匯總在這個工程里面,方便大家參考學(xué)習(xí)勇垛。

3. 基本架構(gòu)

??接下來脖母,我們將進(jìn)入源碼分析階段。不過在這之前闲孤,我們先來了解一下Paging3內(nèi)部整個架構(gòu)谆级,方便后續(xù)在源碼分析的時候,腦海中先有一個輪廓,不至于懵逼肥照。
??根據(jù)我對Paging3框架的理解脚仔,我將Paging3內(nèi)部實現(xiàn)分為兩個部分:

  1. 數(shù)據(jù)請求層。這一層主要負(fù)責(zé)的是數(shù)據(jù)請求舆绎,其中包括加載初始化的數(shù)據(jù)鲤脏,加載更多的數(shù)據(jù),以及多級數(shù)據(jù)源的請求亿蒸。這部分的內(nèi)容主要是由PageFetcher凑兰,PageFetcherSnapshotPagingSource边锁,RemoteMediator等組成姑食。
  2. 數(shù)據(jù)處理和顯示層。這一層主要是負(fù)責(zé)拿到數(shù)據(jù)請求層請求回來的數(shù)據(jù)茅坛,然后進(jìn)行處理和顯示音半。這部分的內(nèi)容主要是由PagingDataAdapterAsyncPagingDataDiffer贡蓖,PagingDataDifferPagePresenter

??我們來分開看一下每一層主要架構(gòu)和聯(lián)系曹鸠。

(1). 數(shù)據(jù)請求層

??數(shù)據(jù)請求層最主要的責(zé)任就是數(shù)據(jù)請求,而數(shù)據(jù)請求的類型在Paging3分為兩種:

  1. 初始化頁面數(shù)據(jù)請求斥铺。
  2. 上一頁或和下一頁數(shù)據(jù)請求彻桃。

??這其中,由PageFetcher提供Api觸發(fā)請求晾蜘,比如說PageFetcher里面有refreshinvalidate兩個方法可以觸發(fā)刷新邏輯邻眷;而更多數(shù)據(jù)請求是通過PageFetcherPagerUiReceiver來首先觸發(fā)。整體邏輯是:由PageFetcher內(nèi)部的Flow創(chuàng)建一個PageFetcherSnapshot對象剔交,數(shù)據(jù)請求的操作通過PageFetcher傳遞到PageFetcherSnapshot里面肆饶,PageFetcherSnapshot內(nèi)部有兩個方法來請求數(shù)據(jù),分別是:

  1. doInitialLoad:表示加載刷新的數(shù)據(jù)岖常。
  2. doLoad:表示加載其他頁的數(shù)據(jù)驯镊。

??所有的數(shù)據(jù)請求都會走到這兩個方法進(jìn)行數(shù)據(jù)請求,PagingSourceRemoteMediator的load方法也是在這兩個方法里面進(jìn)行調(diào)用的竭鞍。

(2). 數(shù)據(jù)處理和顯示層

??刷新請求完成之后板惑,PageFetcher會通過內(nèi)部的Flow發(fā)送一個PagingData對象。而Adapter會通過監(jiān)聽拿到這個PagingData笼蛛,然后進(jìn)行數(shù)據(jù)處理洒放。這其中PagingData內(nèi)部封裝了幾個參數(shù):

  1. PageEvent:內(nèi)部封裝了關(guān)于數(shù)據(jù)的信息。包括當(dāng)前加載類型滨砍,即LoadType(REFRESH,PREPEND,APPEND)惋戏;加載狀態(tài)领追,即CombinedLoadStates(NotLoading,Loading响逢,Error)绒窑;以及更重要的數(shù)據(jù)列表。
  2. UiReceiver:用來觸發(fā)加載其他頁面數(shù)據(jù)的接口舔亭。

??Adapter拿到這個PaingData對象之后些膨,會一路透傳,直到PagingDataDiffercollectFrom方法钦铺。在PagingDataDiffer內(nèi)部订雾,首先會通過PagingData內(nèi)部的一個Flow監(jiān)聽PageEvent,然后不同類型的PageEvent(Insert矛洞,Drop洼哎,LoadStateUpdate)進(jìn)行分發(fā),如果是LoadStateUpdate那么表示是加載狀態(tài)更新的沼本,即會回調(diào)加載狀態(tài)的監(jiān)聽Listener噩峦;如果是其他類型的PageEvent就進(jìn)行數(shù)據(jù)處理,數(shù)據(jù)處理主要是依靠PagePresenter來幫忙抽兆,然后將對應(yīng)的數(shù)據(jù)變化同步到Adapter層面上识补,即調(diào)用Adapter的notify方法。

(3). 枚舉類

??在Paging3的內(nèi)部辫红,有很多的枚舉類凭涂,用來表示某種狀態(tài)。如果我們在看源碼對這些枚舉類不了解厉熟,那么代碼理解起來就比較麻煩导盅,所以我在這里重點解釋一下。

A.LoadType

??即加載類型揍瑟,一共有三個枚舉類白翻,具體名字和含義如下:

名字 含義
REFRESH 刷新
PREPEND 表示加載上一頁數(shù)據(jù)
APPEND 表示加載下一頁數(shù)據(jù)
B.LoadState

??加載狀態(tài),即當(dāng)前加載是什么一個情況绢片,通常來說滤馍,每一種LoadType都對應(yīng)一個LoadState,表示當(dāng)前加載類型的具體狀態(tài)底循。具體名字和含義如下:

名字 含義
NotLoading 表示沒有在加載或者已經(jīng)加載完成巢株。內(nèi)部帶有一個標(biāo)記字段,
endOfPaginationReached,用來表示當(dāng)前是否還有剩余的數(shù)據(jù)需
要加載熙涤,其中false表示還有剩余的數(shù)據(jù)未加載阁苞,true表示沒有
剩余的數(shù)據(jù)困檩。比如說APPEND的狀態(tài)為NotLoading,表示當(dāng)前
加載更多已經(jīng)完成那槽,如果endOfPaginationReached為false悼沿,表示
還有數(shù)據(jù)需要加載,到了合適的時機還有觸發(fā)加載更多骚灸,反之亦然糟趾。
Loading 表示正在加載。
Error 表示加載失敗甚牲。
C.PageEvent

??最終數(shù)據(jù)請求的結(jié)果都會作為PageEvent通過PagingData傳遞到UI層义郑,PageEvent一共三個枚舉狀態(tài),分別如下:

名字 含義
Insert 表示有新的數(shù)據(jù)增加丈钙,其中Refresh,Append,Prepend請求
成功之后都會產(chǎn)生這個事件非驮。
Drop 表示有數(shù)據(jù)需要刪除,這個事件是在AppendPrepend時機
才會產(chǎn)生著恩。
LoadStateUpdate 表示每種加載類型的加載狀態(tài)更新院尔。

??除去這些枚舉類,Paging3內(nèi)部還有各種Helper類喉誊,用來存儲狀態(tài)邀摆,這里就不再過多的介紹,在源碼分析過程中伍茄,如果遇到栋盹,我會進(jìn)行簡單的解釋。

4. 首次加載

??接下來敷矫,我們將進(jìn)入源碼分析階段例获,首先我們來看看首次加載的過程,需要注意的是這里的首次加載泛指進(jìn)入頁面的第一次加載和手動刷新加載曹仗。這里我從兩個方面分析首次加載的過程榨汤,分別是:數(shù)據(jù)請求層數(shù)據(jù)處理和顯示層。首先我們來看一下數(shù)據(jù)請求層的實現(xiàn)怎茫。

(1). 數(shù)據(jù)請求層

??我們在介紹基本使用的時候已經(jīng)提到過收壕,我們需要通過Pager拿到一個Flow對象,Pager的Flow對象其實是從PageFetcher里面獲取的轨蛤,所以我們直接來看PageFetcher里面實現(xiàn)蜜宪。
??在正式介紹源碼之前,我們先來看一下PageFetcher內(nèi)部的兩個Channel對象:

  1. refreshChannel:用來通知觸發(fā)刷新邏輯祥山。其中true表示RemoteMediator也要刷新(如果有的話)圃验,false則表示RemoteMediator不刷新。
  2. retryChannel:用重試刷新缝呕。我們通過Adapter的retry方法澳窑,最終會通過這個Channel來通知重試斧散。

??接下來,我們來看一下PageFetcher內(nèi)部最重要的一個Flow的實現(xiàn):

    val flow: Flow<PagingData<Value>> = channelFlow {
        val remoteMediatorAccessor = remoteMediator?.let {
            RemoteMediatorAccessor(this, it)
        }
        refreshChannel.asFlow()
            .onStart {
                @OptIn(ExperimentalPagingApi::class)
                emit(remoteMediatorAccessor?.initialize() == LAUNCH_INITIAL_REFRESH)
            }
            .scan(null) {
                previousGeneration: PageFetcherSnapshot<Key, Value>?, triggerRemoteRefresh ->
                var pagingSource = generateNewPagingSource(previousGeneration?.pagingSource)
                while (pagingSource.invalid) {
                    pagingSource = generateNewPagingSource(previousGeneration?.pagingSource)
                }

                @OptIn(ExperimentalPagingApi::class)
                val initialKey: Key? = previousGeneration?.refreshKeyInfo()
                    ?.let { pagingSource.getRefreshKey(it) }
                    ?: initialKey

                previousGeneration?.close()

                PageFetcherSnapshot<Key, Value>(
                    initialKey = initialKey,
                    pagingSource = pagingSource,
                    config = config,
                    retryFlow = retryChannel.asFlow(),
                    // Only trigger remote refresh on refresh signals that do not originate from
                    // initialization or PagingSource invalidation.
                    triggerRemoteRefresh = triggerRemoteRefresh,
                    remoteMediatorConnection = remoteMediatorAccessor,
                    invalidate = this@PageFetcher::refresh
                )
            }
            .filterNotNull()
            .mapLatest { generation ->
                val downstreamFlow = if (remoteMediatorAccessor == null) {
                    generation.pageEventFlow
                } else {
                    generation.injectRemoteEvents(remoteMediatorAccessor)
                }
                PagingData(
                    flow = downstreamFlow,
                    receiver = PagerUiReceiver(generation, retryChannel)
                )
            }
            .collect { send(it) }
    }

??初次看這段代碼照捡,可能會有點懵逼颅湘,最初我也是這樣的话侧。不過栗精,大家不用擔(dān)心,我會給大家介紹這個Flow里面做了哪些事情瞻鹏。我們先從宏觀上來看這段代碼都做啥事吧(這里為了簡單悲立,我們先把RemoteMediator相關(guān)的忽略,后續(xù)有內(nèi)容專門來介紹它新博。)薪夕,分別:

  1. 在scan方法里面,創(chuàng)建了PageFetcherSnapshot對象赫悄。主要分為三步:首先原献,通過傳入進(jìn)來的工廠函數(shù)創(chuàng)建一個PagingSource,同時如果還有之前的PageFetcherSnapshot存在埂淮,需要進(jìn)行一些清理工作(scan方法內(nèi)部有一個size 為1的Buffer姑隅,會緩存上一個PageFetcherSnapshot對象);其次調(diào)用refreshKeyInfo方法拿到刷新的key倔撞;最后就是創(chuàng)建了一個PageFetcherSnapshot讲仰,同時給PageFetcherSnapshot傳入可能會用到的參數(shù)。
  2. 通過map函數(shù)將相關(guān)事件轉(zhuǎn)為成為一個PagingData對象痪蝇,同時還有PagingData傳入兩個參數(shù)鄙陡,分別是一個PageEvent的Flow對象,下游(UI 層)可以用來監(jiān)聽PageEvent 的發(fā)送躏啰;其次趁矾,就是構(gòu)建了一個PagerUiReceiver對象,用來給下游(UI 層)來觸發(fā)加載下一頁數(shù)據(jù)和嘗試重試加載给僵。
  3. 通過send方法將創(chuàng)建好的PagingData發(fā)送出去的毫捣。

??我們都知道,F(xiàn)low是冷流想际,即只有在收集的時候才會觸發(fā)上面一系列的流程培漏。所以我們在Activity/Fragment 里面調(diào)用Flow的collectLatest方法自然觸發(fā)了上面流程,從而開始初始化加載胡本。
??關(guān)于上面的代碼牌柄,大家還有需要注意一點的是,PagingData的PageEvent是從PageFetcherSnapshot獲取的侧甫,我們在前面介紹過珊佣,PageFetcherSnapshot的工作主要負(fù)責(zé)加載數(shù)據(jù)蹋宦,同時將加載完成的數(shù)據(jù)通過發(fā)送一個PageEvent來通知到下游。
??我們接下來看一下PageFetcherSnapshotpageEventFlow參數(shù)咒锻,因為所有的事件都是通過它發(fā)送到下游去的冷冗。

    val pageEventFlow: Flow<PageEvent<Value>> = cancelableChannelFlow(pageEventChannelFlowJob) {
        // Start collection on pageEventCh, which the rest of this class uses to send PageEvents
        // to this flow.
        launch {
            pageEventCh.consumeAsFlow().collect {
                // Protect against races where a subsequent call to submitData invoked close(),
                // but a pageEvent arrives after closing causing ClosedSendChannelException.
                try {
                    send(it)
                } catch (e: ClosedSendChannelException) {
                    // Safe to drop PageEvent here, since collection has been cancelled.
                }
            }
        }
        // ......(retry 加載)
        // ......(Remote Mediator Refresh)

        // Setup finished, start the initial load even if RemoteMediator throws an error.
        doInitialLoad(state)

        // ......(監(jiān)聽下游發(fā)送的過來的事件,嘗試加載上一頁或者下一頁刷劇)
    }

??pageEventFlow的代碼較多惑艇,我刪除一些我們現(xiàn)在不用關(guān)心的代碼蒿辙,避免影響我們分析整個流程。我們從上面已有的代碼滨巴,我們看出來幾點:

  1. pageEventCh是用來發(fā)送PageEvent的思灌,這里發(fā)送的PageEvent會直接到達(dá)UI 層。不過需要注意的是恭取,這里發(fā)送的PageEvent不僅僅是Insert泰偿,還有其他類型的(Drop和LoadStateUpdate)。
  2. 調(diào)用了doInitialLoad方法蜈垮,進(jìn)行數(shù)據(jù)請求耗跛。

??接下來我們來看doInitialLoad方法的實現(xiàn)。先直接看代碼:

    private suspend fun doInitialLoad(
        state: PageFetcherSnapshotState<Key, Value>
    ) {
        // 設(shè)置狀態(tài)攒发,當(dāng)前正在刷新调塌。
        stateLock.withLock { state.setLoading(REFRESH) }
        val params = loadParams(REFRESH, initialKey)
        // 調(diào)用pagingSource的load 方法,進(jìn)行網(wǎng)絡(luò)請求
        when (val result = pagingSource.load(params)) {
            is Page<Key, Value> -> {
                // 將請求的結(jié)果插入到PageFetcherSnapshotState里面
                val insertApplied = stateLock.withLock { state.insert(0, REFRESH, result) }
                // 更新loadType 對應(yīng)的loadState
                stateLock.withLock {
                    state.setSourceLoadState(REFRESH, NotLoading.Incomplete)
                    if (result.prevKey == null) {
                        state.setSourceLoadState(
                            type = PREPEND,
                            newState = when (remoteMediatorConnection) {
                                null -> NotLoading.Complete
                                else -> NotLoading.Incomplete
                            }
                        )
                    }
                    if (result.nextKey == null) {
                        state.setSourceLoadState(
                            type = APPEND,
                            newState = when (remoteMediatorConnection) {
                                null -> NotLoading.Complete
                                else -> NotLoading.Incomplete
                            }
                        )
                    }
                }
                // 通知UI層進(jìn)行數(shù)據(jù)已經(jīng)更新晨继。
                if (insertApplied) {
                    stateLock.withLock {
                        with(state) {
                            pageEventCh.send(result.toPageEvent(REFRESH))
                        }
                    }
                }
                // ......(remoteMediator的調(diào)用烟阐,先忽略)
            }
            is LoadResult.Error -> stateLock.withLock {
                val loadState = Error(result.throwable)
                if (state.setSourceLoadState(REFRESH, loadState)) {
                    pageEventCh.send(LoadStateUpdate(REFRESH, false, loadState))
                }
            }
        }
    }

??doInitialLoad方法所做事情比較簡單,我們來看一下:

  1. 首先調(diào)用PageFetcherSnapshotState的setLoading方法表示當(dāng)前正在刷新紊扬,在setLoad方法里面會通過pageEventCh發(fā)送一個LoadStateUpdate事件蜒茄,來通知UI 層加載狀態(tài)已經(jīng)變化了。
  2. 調(diào)用PagingSource的load 方法餐屎,進(jìn)行數(shù)據(jù)請求檀葛。這個數(shù)據(jù)可能是從網(wǎng)絡(luò)上請求數(shù)據(jù),也有可能是從數(shù)據(jù)庫里面請求數(shù)據(jù)腹缩,具體得看PagingSource的定義屿聋。
  3. 根據(jù)load返回的結(jié)果進(jìn)行分情況討論。如果是Error藏鹊,那么就會給下游發(fā)送請求失敗的PageEvent润讥;如果是請求成功,即返回的是Page盘寡,那么就分為幾步來進(jìn)行楚殿。首先是,將請求的結(jié)果存儲到PageFetcherSnapshotState里面去竿痰;其次返回結(jié)果傳入的nextKey和prevKey來更新每個LoadType下的LoadState脆粥,以保證后續(xù)的加載更多能夠正常進(jìn)行砌溺。
  4. 發(fā)送一個PageEvent,通知UI層數(shù)據(jù)更新变隔。

??doInitialLoad方法所做之事便如上內(nèi)容规伐,在這里,大家發(fā)現(xiàn)了一個PageFetcherSnapshotState類匣缘,肯定有疑惑這個類是干嘛的猖闪,我在這里簡單的解釋一下。
??通過官方的注釋孵户,我們可以知道這個類主要是來記錄PageFetcherSnapshot的狀態(tài),那么記錄都是什么狀態(tài)呢萧朝?

  1. 數(shù)據(jù)相關(guān)的信息。PageFetcherSnapshotState內(nèi)部有一個_pages變量夏哭,記錄的是已經(jīng)加載的頁面數(shù)據(jù),這個我們從doInitialLoad方法里面也可以看到献联,請求完成之后會調(diào)用PageFetcherSnapshotState方法竖配,目的就是將數(shù)據(jù)存儲到PageFetcherSnapshotState。還有其他數(shù)據(jù)相關(guān)信息里逆,比如說當(dāng)前占位符的數(shù)量进胯,即placeholdersBeforeplaceholdersAfter,這個變量跟我們之前在介紹LoadResult.PageitemsBeforeitemsBefore是同一個意思原押。以及還有其他信息胁镐,這里就不過多的介紹。
  2. 每種LoadType對應(yīng)的LoadState诸衔。內(nèi)部有一個sourceLoadStates變量盯漂,記錄三種LoadType 的狀態(tài)。外部通常通過setSourceLoadState來更新對應(yīng)的值笨农,同理就缆,我們可以在doInitialLoad方法看到它被調(diào)用的影子。

(2). 數(shù)據(jù)處理和顯示層

??我們從上面知道了首次加載的數(shù)據(jù)會通過發(fā)送一個PageEvent傳送到數(shù)據(jù)處理和顯示層(即UI 層谒亦,為了簡單竭宰,后文統(tǒng)一使用UI層表示)。
??繁瑣的源碼追蹤工作份招,我們這里不做了切揭,我們直接到PagingDataDiffercollectFrom方法里面去,因為在方法里面對PageEvent事件進(jìn)行監(jiān)聽锁摔。我們直接看代碼:

    suspend fun collectFrom(pagingData: PagingData<T>) = collectFromRunner.runInIsolation {
        // 存下UiReceiver廓旬,以備后續(xù)觸發(fā)加載更多。
        receiver = pagingData.receiver
        pagingData.flow.collect { event ->
            withContext<Unit>(mainDispatcher) {
                // 如果是Insert鄙漏,并且是刷新操作。
                if (event is PageEvent.Insert && event.loadType == REFRESH) {
                    lastAccessedIndexUnfulfilled = false
                    // 創(chuàng)建一個PagePresenter用以存儲和處理數(shù)據(jù)
                    val newPresenter = PagePresenter(event)
                    // 將舊PagePresenter里面數(shù)據(jù)遷移到新的PagePresenter
                    val transformedLastAccessedIndex = presentNewList(
                        previousList = presenter,
                        newList = newPresenter,
                        newCombinedLoadStates = event.combinedLoadStates,
                        lastAccessedIndex = lastAccessedIndex
                    )
                    presenter = newPresenter
                    // 回調(diào)Listener
                    dataRefreshedListeners.forEach { listener ->
                        listener(event.pages.all { page -> page.data.isEmpty() })
                    }
                    dispatchLoadStates(event.combinedLoadStates)

                    // 嘗試觸發(fā)加載下一頁(上一頁)的數(shù)據(jù)
                    transformedLastAccessedIndex?.let { newIndex ->
                        lastAccessedIndex = newIndex
                        receiver?.accessHint(
                            newPresenter.viewportHintForPresenterIndex(newIndex)
                        )
                    }
                } else {
                  // Append 或者Prepend
                }
            }
        }
    }

??collectFrom方法的代碼比較長瞎领,主要是處理兩部分的內(nèi)容:Refresh和非Refresh瓮增。這里,我們將非Refresh的代碼省略旁赊,只看Refresh部分的代碼。collectFrom方法針對Refresh做了如下幾件事:

  1. 存下UiReceiver椅野,以備后續(xù)觸發(fā)加載更多终畅。這個我們說了很多遍,這里就不過多的說了竟闪,后續(xù)在介紹加載更多的過程時离福,會再次看到的。
  2. 創(chuàng)建一個新的新PagePresenter炼蛤,用來存儲和處理數(shù)據(jù)妖爷。
  3. 調(diào)用presentNewList方法,將舊的Presenter數(shù)據(jù)遷移到新的Presenter理朋。主要實現(xiàn)是通過DiffUtil來計算Diff絮识,進(jìn)而通知Adapter notify,有興趣的同學(xué)可以看看方法的實現(xiàn)嗽上,這里就不介紹了次舌。同時看到這個,可能有人會有疑惑兽愤,Refresh都是將原來的數(shù)據(jù)清空彼念,然后插入新的數(shù)據(jù),為啥要把老的數(shù)據(jù)遷移到新的數(shù)據(jù)里面呢浅萧?在平常中逐沙,我們對Refresh的理解是沒有問題的,但是在Paging3中惯殊,Refresh 操作不一定會清空老的數(shù)據(jù)酱吝,這一點一定記住。
  4. 回調(diào)對應(yīng)的Listener土思。
  5. 嘗試觸發(fā)加載下一頁(下一頁數(shù)據(jù))的數(shù)據(jù)务热。transformedLastAccessedIndex表示將在老的數(shù)據(jù)里面的lastAccessedIndex(上一次訪問的位置,在get方法記錄的)在新的數(shù)據(jù)中的位置己儒。如果當(dāng)前數(shù)據(jù)量還不夠當(dāng)前訪問崎岂,需要加載更多的數(shù)據(jù)以滿足要求。

??相信大家在這里看到一個新的類--PagePresenter闪湾,想要知道這個類的作用是什么冲甘?我在這里簡單的解釋一下。

PagePresenter內(nèi)部存儲了Adapter所有的數(shù)據(jù),可以簡單的理解為時Adapter的Data List江醇。因為從源碼中中看出來濒憋,Adapter 獲取數(shù)據(jù)和計算當(dāng)前數(shù)據(jù)總數(shù)量都是從通過PagePresenter。同時陶夜,PagePresenter 還擔(dān)任著處理PageEvent的責(zé)任凛驮,因為內(nèi)部有一個processEvent方法,這個方法的作用根據(jù)不同的PageEvent進(jìn)行不同的處理条辟,其中Insert表示要新增數(shù)據(jù)黔夭;Drop表示要刪除數(shù)據(jù);LoadStateUpdate表示要更新狀態(tài)。除此之外羽嫡,這個類還有很多有意思的東西本姥,比如說ViewportHint的構(gòu)造等,這里就不過多的介紹了杭棵。

?UI層對Refrsh處理的過程便是如上的內(nèi)容婚惫。到此,對首次加載的整個流程的分析就結(jié)束了颜屠,在這里辰妙,我做一個小小的總結(jié),方便大家腦海中能把這部分的內(nèi)容的串起來甫窟。

首先,在UI層蛙婴,我們在從ViewModel 拿到一個Flow粗井,這個Flow對象是用來監(jiān)聽PagingData。正常來說街图,只有Refresh才會發(fā)送一個PagingData,Append 和Prepend 不會發(fā)送PagingData浇衬。PagingDataDiffer會從PagingData里面拿到一個發(fā)送PageEvent的Flow,當(dāng)數(shù)據(jù)請求完成餐济,這個Flow會收到一個Insert類型的事件耘擂,這個事件里面封裝了請求回來的數(shù)據(jù)。拿到這個PageEvent絮姆,會創(chuàng)建一個PagePresenter來存儲和處理數(shù)據(jù)醉冤,以及處理對應(yīng)的PageEvent。
其次篙悯,在數(shù)據(jù)請求層蚁阳,PageFetcherSnapshot通過調(diào)用doInitialLoad方法,進(jìn)而調(diào)用PagingSource的load方法鸽照。load方法返回了一個Result螺捐,PageFetcherSnapshot會將這個Result轉(zhuǎn)換成為一個PageEvent,發(fā)送給UI層。

5. 加載更多

??接下來定血,我們將分析另一個加載的過程--加載更多赔癌。為了簡單起見,本章節(jié)的內(nèi)容中以加載下一頁的數(shù)據(jù)表示加載更多澜沟,即Append操作灾票。
??其實,我們在分析首次加載的過程中倔喂,已經(jīng)涉及到了很多這部分的內(nèi)容铝条,當(dāng)然在這之前也留下很多的伏筆。這里席噩,我們就來詳細(xì)的看一下加載更多的內(nèi)容班缰。
??在首次加載中,我們是先介紹數(shù)據(jù)請求層的內(nèi)容悼枢,再介紹UI層的內(nèi)容埠忘。在本章節(jié)中,我們反向操作馒索,先介紹UI層的內(nèi)容莹妒,再介紹數(shù)據(jù)請求層的內(nèi)容。為啥要這樣呢绰上?因為首次加載是一個主動過程旨怠,不需要讓UI層自己觸發(fā)(嚴(yán)格來說,也是UI層自己觸發(fā)的)蜈块,而加載更多確實是被動過程鉴腻,需要Ui層自己去觸發(fā)。

(1). UI 層觸發(fā)

??總的來說百揭,觸發(fā)加載更多的地方很少爽哎,很簡單,我將其分為兩類:

  1. 調(diào)用Adapter的getItem方法器一,會嘗試加載更多的數(shù)據(jù)课锌。其中這個getItem方法的調(diào)用包括RecyclerView 自己調(diào)用,還有一個就是我們手動調(diào)用祈秕。所以渺贤,如果使用Paging3,不要隨意的調(diào)用getItem方法踢步,切記切記癣亚!
  2. 正在請求的數(shù)據(jù)已經(jīng)回來,但是發(fā)現(xiàn)已有的數(shù)據(jù)不夠訪問位置的要求获印,會自己請求述雾。比如說街州,當(dāng)前我們訪問的index是100,數(shù)據(jù)請求回來發(fā)現(xiàn)才80條數(shù)據(jù)玻孟,還不夠數(shù)量唆缴,會繼續(xù)請求。

??關(guān)于第一點黍翎,我不用解釋什么面徽。但是第二個點,我們需要重點分析一下匣掸,了解它的細(xì)節(jié)趟紊。在PagingDataDiffer內(nèi)部有兩個變量:

  1. lastAccessedIndex:表示上一次訪問的位置。
  2. lastAccessedIndexUnfulfilled:表示上一次訪問是否命中占位符碰酝。通俗來講就是霎匈,上一次訪問的位置是否超出已有數(shù)據(jù)的邊界,true表示超出邊界送爸,false表示沒有超出邊界铛嘱。但是從源碼來看,Google爸爸在PagingDataDiffer的get方法里面直接設(shè)置為true袭厂,所以可以理解這個變量應(yīng)該默認(rèn)每次訪問都會超出邊界墨吓。不過這樣讓人很疑惑,不知道Google是出于什么考慮纹磺。

??當(dāng)數(shù)據(jù)請求回來之后(包括Refresh和非Refresh)帖烘,會根據(jù)上一次的訪問位置會再次詢問是否還可以加載下一頁的數(shù)據(jù)。代碼如下:

    suspend fun collectFrom(pagingData: PagingData<T>) = collectFromRunner.runInIsolation {
        // ······
        pagingData.flow.collect { event ->
            withContext<Unit>(mainDispatcher) {
                if (event is PageEvent.Insert && event.loadType == REFRESH) {
                    // ······
                    transformedLastAccessedIndex?.let { newIndex ->
                        lastAccessedIndex = newIndex
                        // 嘗試請求下一頁數(shù)據(jù)
                        receiver?.accessHint(
                            newPresenter.viewportHintForPresenterIndex(newIndex)
                        )
                    }
                } else {
                        // ·······
                        if (!canContinueLoading) {
                           // ·······
                        } else if (lastAccessedIndexUnfulfilled) {
                             // ·······
                            if (shouldResendHint) {
                                // 嘗試請求下一頁數(shù)據(jù)
                                receiver?.accessHint(
                                    presenter.viewportHintForPresenterIndex(lastAccessedIndex)
                                )
                            } else {
                                // lastIndex fulfilled, so reset lastAccessedIndexUnfulfilled.
                                lastAccessedIndexUnfulfilled = false
                            }
                        }
                    }
                }
            }
        }
    }

??關(guān)于這里面的計算細(xì)節(jié)橄杨,我們就不細(xì)扣了蚓让,有興趣的同學(xué)可以去看看。不過這里讥珍,我們發(fā)現(xiàn)在調(diào)用UiReceiveraccessHint方法時,創(chuàng)建了一個ViewportHint對象窄瘟,那么這個ViewportHint對象有什么用呢衷佃?我們先來看一下這個類的幾個成員變量:

名字 含義
pageOffset 表示當(dāng)前訪問index所在的頁面index。我們都知道蹄葱,Paging
里面的數(shù)據(jù)都是分頁存儲氏义,類似于List<List<Data>>結(jié)構(gòu),
而這個pageOffset表示的時List<Data>的index图云。
indexInPage 表示當(dāng)前訪問index在頁面內(nèi)的index惯悠。
presentedItemsBefore 表示當(dāng)前訪問index之前非展位符的數(shù)量。比如說竣况,當(dāng)前訪
問位置是100克婶,當(dāng)時前置占位符有40個,那么presentedItemsBefore
就等于60(100 - 40)。同時如果這個變量如果為0情萤,表示訪問
位置恰好是上邊界鸭蛙;如果是正數(shù),那么表示訪問位置正好在
數(shù)據(jù)范圍內(nèi)筋岛;如果是負(fù)數(shù)娶视,訪問位置就是占位符。
presentedItemsAfter presentedItemsBefore睁宰,只是presentedItemsAfter表示的是
下邊界肪获。
originalPageOffsetFirst 當(dāng)前數(shù)據(jù)第一頁面的頁碼。不一定都為0柒傻,因為有maxSize
的存在孝赫,maxSize會丟棄前面已失效的數(shù)據(jù)(用null來填充)。
originalPageOffsetLast originalPageOffsetFirst,只是originalPageOffsetLast表示
最后一頁的頁面诅愚。

??關(guān)于上面的解釋寒锚,我猜測有些同學(xué)可能還會疑惑,我在這里在補充幾句违孝。

在Paging3內(nèi)刹前,存在三種index,分別是:

  1. 絕對index雌桑,我們可以這樣來理解這個index喇喉,就是將所有的數(shù)據(jù)拍平在一個List里面,每個item的Index就是絕對index校坑。
  2. 頁面index拣技,顧名思義,就是該頁面數(shù)據(jù)在所有頁面的index耍目。上述的pageOffset膏斤,originalPageOffsetFirstoriginalPageOffsetLast都是頁面index邪驮。
  3. 頁內(nèi)Index(相對index)莫辨,就是指該Item所在頁面里面內(nèi)的index。上述的indexInPage便是頁內(nèi)index毅访。

(2). 數(shù)據(jù)請求層沮榜。

??UI層通過調(diào)用UiReceiveraccessHint方法,并且通過一個ViewportHint對象來攜帶一些信息喻粹。而accessHint方法便是Ui層和數(shù)據(jù)請求層的溝通橋梁蟆融,數(shù)據(jù)請求層監(jiān)聽到這個方法的回調(diào),會向一個名為hintChannel通道發(fā)送一個事件:

    fun accessHint(viewportHint: ViewportHint) {
        lastHint = viewportHint
        @OptIn(ExperimentalCoroutinesApi::class)
        hintChannel.offer(viewportHint)
    }

注意上述的accessHint方法在PageFetcherSnapshot里面守呜,調(diào)用關(guān)系:PagerUiReceiver#accessHint -> PageFetcherSnapshot#accessHint型酥。

??那么哪里在監(jiān)聽這個事件呢山憨?是在startConsumingHints方法里面。不過在看這個方法之前冕末,我們先回過頭來看一下pageEventFlow的定義萍歉,之前我們看的時候省略了加載更多的代碼。

    val pageEventFlow: Flow<PageEvent<Value>> = cancelableChannelFlow(pageEventChannelFlowJob) {
        // ······
        // 加載更多
        if (stateLock.withLock { state.sourceLoadStates.get(REFRESH) } !is Error) {
            startConsumingHints()
        }
    }

??通過上面的代碼档桃,我們可以看出來枪孩,實際上在進(jìn)行Refresh操作的時候,就已經(jīng)在監(jiān)聽加載更多操作藻肄,當(dāng)然這個實現(xiàn)我們也可以猜想得到的蔑舞,這里只不過驗證一下具體實現(xiàn)而已。我們來看startConsumingHints:

    @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
    private fun CoroutineScope.startConsumingHints() {
        // 1. 嘗試觸發(fā)Refresh操作
        if (config.jumpThreshold != COUNT_UNDEFINED) {
            launch {
                hintChannel.asFlow()
                    .filter { hint ->
                        hint.presentedItemsBefore * -1 > config.jumpThreshold ||
                            hint.presentedItemsAfter * -1 > config.jumpThreshold
                    }
                    .collectLatest { invalidate() }
            }
        }
        // 2. 加載上一頁的數(shù)據(jù)
        launch {
            state.consumePrependGenerationIdAsFlow()
                .collectAsGenerationalViewportHints(PREPEND)
        }
        // 3. 加載下一頁的數(shù)據(jù)
        launch {
            state.consumeAppendGenerationIdAsFlow()
                .collectAsGenerationalViewportHints(APPEND)
        }
    }

??startConsumingHints方法的實現(xiàn)很簡單嘹屯,但是這里有很多的細(xì)節(jié)需要注意:

  1. 這里有一個jumpThreshold攻询,關(guān)于這個變量的含義,我們之前已經(jīng)說過了州弟,具體就是當(dāng)前訪問的數(shù)據(jù)已經(jīng)超過了預(yù)先設(shè)置的閾值钧栖,那么直接就用Refresh操作來加載下一頁數(shù)據(jù)。這個會在RemoteMediator里面會使用得到婆翔。
  2. 通過調(diào)用collectAsGenerationalViewportHints實現(xiàn)了PREPENDAPPEND兩個操作拯杠。關(guān)于consumePrependGenerationIdAsFlowconsumeAppendGenerationIdAsFlow方法,我們這里就不講解了啃奴,因為這個涉及到maxSize和DropEvent潭陪,所涉及的內(nèi)容就非常多,這里就不展開了最蕾。

??我們這里直接使用來看collectAsGenerationalViewportHints方法:

    private suspend fun Flow<Int>.collectAsGenerationalViewportHints(
        loadType: LoadType
    ) = flatMapLatest { generationId ->
        // Reset state to Idle and setup a new flow for consuming incoming load hints.
        // Subsequent generationIds are normally triggered by cancellation.
        stateLock.withLock {
            // Skip this generationId of loads if there is no more to load in this
            // direction. In the case of the terminal page getting dropped, a new
            // generationId will be sent after load state is updated to Idle.
            if (state.sourceLoadStates.get(loadType) == NotLoading.Complete) {
                return@flatMapLatest flowOf()
            } else if (state.sourceLoadStates.get(loadType) !is Error) {
                state.setSourceLoadState(loadType, NotLoading.Incomplete)
            }
        }

        @OptIn(FlowPreview::class)
        hintChannel.asFlow()
            // Prevent infinite loop when competing PREPEND / APPEND cancel each other
            .drop(if (generationId == 0) 0 else 1)
            .map { hint -> GenerationalViewportHint(generationId, hint) }
    }
        // Prioritize new hints that would load the maximum number of items.
        .runningReduce { previous, next ->
            if (next.shouldPrioritizeOver(previous, loadType)) next else previous
        }
        .conflate()
        .collect { generationalHint ->
            doLoad(state, loadType, generationalHint)
        }

??關(guān)于collectAsGenerationalViewportHints方法依溯,內(nèi)部做了如下幾件事:

  1. 通過flatMapLatest操作流拍平。因為hintChannel.asFlow()方法返回的一個Flow瘟则,所以需要拍平黎炉。其次,我們這里需要注意一下醋拧,如果generationId為0拜隧,表示當(dāng)前沒有進(jìn)行Drop的操作,那么就不跳過第一個趁仙;如果不為0,那么進(jìn)行了Drop操作垦页,那么就跳過第一個雀费,因為在進(jìn)行Drop操作時,這里會收到一個generationId痊焊,關(guān)于這個點盏袄,待會我單獨講解忿峻,這里先不展開。
  2. 通過conflate操作符跳過之前發(fā)送的事件辕羽,保證只會取到一個事件逛尚。這個類似于RxJava里面的被壓問題,有興趣的同學(xué)可以看看這個操作符的原理刁愿,這里就不講解了绰寞。
  3. 最后,就是調(diào)用doLoad方法數(shù)據(jù)請求铣口。

??說了這么多滤钱,拋開中間很多沒用的信息,其實從調(diào)用UiReceiveraccessHint方法開始脑题,最終會調(diào)用到doLoad方法進(jìn)行網(wǎng)絡(luò)請求件缸。
??好了接下來,我們將重點分析doLoad方法叔遂,此方法涉及的內(nèi)容非常的多他炊,大家要有心里準(zhǔn)備,不過我會盡最大的努力給大家解釋清楚已艰。

    private suspend fun doLoad(
        state: PageFetcherSnapshotState<Key, Value>,
        loadType: LoadType,
        generationalHint: GenerationalViewportHint
    ) {
        // 1. 計算已經(jīng)加載的數(shù)量痊末。
        var itemsLoaded = 0
        stateLock.withLock {
            when (loadType) {
                PREPEND -> {
                    val firstPageIndex =
                        state.initialPageIndex + generationalHint.hint.originalPageOffsetFirst - 1
                    for (pageIndex in 0..firstPageIndex) {
                        itemsLoaded += state.pages[pageIndex].data.size
                    }
                }
                APPEND -> {
                    val lastPageIndex =
                        state.initialPageIndex + generationalHint.hint.originalPageOffsetLast + 1
                    for (pageIndex in lastPageIndex..state.pages.lastIndex) {
                        itemsLoaded += state.pages[pageIndex].data.size
                    }
                }
                REFRESH -> throw IllegalStateException("Use doInitialLoad for LoadType == REFRESH")
            }
        }
        // 2. 通過已經(jīng)加載的數(shù)量獲取key。
        var loadKey: Key? = stateLock.withLock {
            state.nextLoadKeyOrNull(loadType, generationalHint, itemsLoaded)
                ?.also { state.setLoading(loadType) }
        }
        var endOfPaginationReached = false
        loop@ while (loadKey != null) {
            val params = loadParams(loadType, loadKey)
            // 3. 加載數(shù)據(jù)
            val result: LoadResult<Key, Value> = pagingSource.load(params)
            when (result) {
                is Page<Key, Value> -> {
                    // ······
                    // 4. 插入數(shù)據(jù)
                    val insertApplied = stateLock.withLock {
                        state.insert(generationalHint.generationId, loadType, result)
                    }
                    // Break if insert was skipped due to cancellation
                    if (!insertApplied) break@loop
                    itemsLoaded += result.data.size
                    // 5. 如果nextKey為空旗芬,將endOfPaginationReached設(shè)置為true舌胶,
                    // 表示當(dāng)前LoadType已經(jīng)沒有數(shù)據(jù)了。
                    if ((loadType == PREPEND && result.prevKey == null) ||
                        (loadType == APPEND && result.nextKey == null)
                    ) {
                        endOfPaginationReached = true
                    }
                }
                // 省略Error的代碼疮丛。
            }
            val dropType = when (loadType) {
                PREPEND -> APPEND
                else -> PREPEND
            }
            // 6. 進(jìn)行Drop操作
            stateLock.withLock {
                state.dropEventOrNull(dropType, generationalHint.hint)?.let { event ->
                    state.drop(event)
                    pageEventCh.send(event)
                }
                loadKey = state.nextLoadKeyOrNull(loadType, generationalHint, itemsLoaded)
                // Update load state to success if this is the final load result for this
                // load hint, and only if we didn't error out.
                if (loadKey == null && state.sourceLoadStates.get(loadType) !is Error) {
                    state.setSourceLoadState(
                        type = loadType,
                        newState = when {
                            endOfPaginationReached -> NotLoading.Complete
                            else -> NotLoading.Incomplete
                        }
                    )
                }
                // Send page event for successful insert, now that PagerState has been updated.
                val pageEvent = with(state) {
                    result.toPageEvent(loadType)
                }
                // 7. 發(fā)送事件到UI層幔嫂。
                pageEventCh.send(pageEvent)
            }
            // 省略RemoteMediator的代碼。
        }
    }

??通過上面的代碼誊薄,以及我添加的注釋履恩,我們可以知道,doLoad方法一共做了4件事:

  1. 計算已經(jīng)加載數(shù)據(jù)的數(shù)量呢蔫,然后獲取對應(yīng)的key切心,用以后面的數(shù)據(jù)請求。在這里片吊,就用到了之前在創(chuàng)建ViewportHint所攜帶的信息绽昏。
  2. 調(diào)用PagingSource的load方法,進(jìn)行數(shù)據(jù)請求俏脊。如果請求成功全谤,會通過PageFetcherSnapshotStateinsert方法把對應(yīng)的數(shù)據(jù)插入進(jìn)去,這個操作我們在Refresh里面看到過了爷贫,這里就不講解了认然。
  3. 進(jìn)行Drop操作补憾,嘗試丟棄失效的頁面。這一步非常的重要卷员,這個對于我們理解前面所說的startConsumingHints有很大的幫助盈匾。這里我先不講解它,后續(xù)會重點分析它毕骡。
  4. 通過pageEventCh發(fā)送一個PageEvent用來告知UI層削饵,數(shù)據(jù)已經(jīng)在加載完成。這個我們在前面分析過了挺峡,這里也不再講解了葵孤。

??總的來說,doLoad方法的實現(xiàn)還是比較簡單的橱赠,當(dāng)然這里省略很多的細(xì)節(jié)尤仍,比如說Drop操作。不過狭姨,整個流程我們理解還是比較清晰宰啦,這里我對加載更多做一個簡單的總結(jié),方便大家來理解饼拍。

加載更多是一個UI層主動赡模,數(shù)據(jù)請求層被動的過程。UI層通過調(diào)用UiReceiveraccessHint方法來告知數(shù)據(jù)請求層需要進(jìn)行加載更多的數(shù)據(jù)請求师抄,在調(diào)用的同時UI層通過傳遞一個ViewportHint對象用來攜帶一些關(guān)鍵信息漓柑。數(shù)據(jù)請求層監(jiān)聽到這個行為,并且拿到ViewportHint對象叨吮,通過一系列的計算獲取一個key辆布,進(jìn)而調(diào)用PagingSourceload方法進(jìn)行數(shù)據(jù)請求,數(shù)據(jù)請求完成之后茶鉴,進(jìn)行了一些常規(guī)操作之后锋玲,通過pageEventCh發(fā)送一個PageEvent用來告知UI層,數(shù)據(jù)已經(jīng)在加載完成涵叮。

??如上便是加載更多的整個過程惭蹂。接下來,為了大家理解更加的深刻割粮,我將對drop操作進(jìn)行分析盾碗,算是額外的福利內(nèi)容,因為內(nèi)容大綱并沒有寫這個舀瓢。

6. Drop操作

??前面在分析加載更多的時候置尔,反復(fù)的提到Drop操作,包括在介紹PageEvent時,也介紹Drop事件榜轿。那么到底什么是Drop,什么時候進(jìn)行Drop操作呢朵锣?
??一言以蔽之谬盐,Drop可以理解為裁剪,當(dāng)我們在創(chuàng)建PagingConfig時诚些,有一個配置項--maxSize飞傀,表示當(dāng)前數(shù)據(jù)總量。需要特別注意的是诬烹,這個maxSize表示的意思并不是數(shù)據(jù)最大的數(shù)量砸烦,而是Adapter內(nèi)部的List可以存儲有效數(shù)據(jù)(非空數(shù)據(jù))的最大數(shù)據(jù)量。比如說绞吁,我們將maxSize 設(shè)置為200幢痘,那么表示Adapter內(nèi)部存儲只能200條,超過200條之后從頭開始丟棄家破⊙账担回到PagePresenter里面來,這個類里面有三個變量用來統(tǒng)計數(shù)據(jù)總量汰聋,但是統(tǒng)計的數(shù)據(jù)是不同的门粪,如下:

名字 含義
storageCount 真實的數(shù)據(jù)總量,不包括為空的數(shù)據(jù)量烹困。
placeholdersBefore 前置占位符的數(shù)量玄妈,這個范圍里面的數(shù)據(jù)獲取都為空。
placeholdersAfter 后置占位符的數(shù)量髓梅。

??Adapter 在統(tǒng)計數(shù)據(jù)總量(getItemCount)時拟蜻,是通過PagePresentersize方法來獲取的。即上述三個總量的和:

    override val size: Int
        get() = placeholdersBefore + storageCount + placeholdersAfter

??而PagingConfig里面的maxSize限制的是storageCount女淑,這一點大家一定要清楚瞭郑。
??其次,maxSize只在enablePlaceholders為true生效鸭你,切記切記G拧!
??回到doLoad方法袱巨,它之所以要在數(shù)據(jù)請求之后阁谆,且插入之后,進(jìn)行裁剪操作愉老,是為了讓maxSize 這個配置項生效场绿,不能讓總數(shù)據(jù)量超過設(shè)置的閾值。那么它是在怎么進(jìn)行裁剪的呢嫉入?主要分為兩步(下面代碼是doLoad方法部分代碼片段):

// 1. 通過dropEventOrNull方法計算需要裁剪的數(shù)據(jù)
state.dropEventOrNull(dropType, generationalHint.hint)?.let { event ->
    // 2.裁剪數(shù)據(jù)
    state.drop(event)
    pageEventCh.send(event)
}

??這部分所做內(nèi)容的如下:

  1. 調(diào)用dropEventOrNull方法計算需要裁剪的數(shù)據(jù)焰盗,如果需要裁剪璧尸,那么會返回一個Drop的PageEvent;如果不需要裁剪,那么就會返回為空熬拒。
  2. 通過DropEvent裁剪數(shù)據(jù)爷光。首先是調(diào)用了PageFetcherSnapshotState的drop方法,將內(nèi)部存儲對應(yīng)的數(shù)據(jù)刪除澎粟;其次就發(fā)送一個PageEvent到UI層蛀序,告知UI層也要對應(yīng)的刪除。

??我們來看看PageFetcherSnapshotState的drop方法的實現(xiàn):

    fun drop(event: PageEvent.Drop<Value>) {
        // .....
        when (event.loadType) {
            // 省略PREPEND的代碼
            APPEND -> {
                repeat(event.pageCount) { _pages.removeAt(pages.size - 1) }

                placeholdersAfter = event.placeholdersRemaining

                appendLoadId++
                appendLoadIdCh.offer(appendLoadId)
            }
           // ......
        }
    }

??這里我們只看APPEND操作活烙,這個方法做了兩件事徐裸,兩件事都非常重要:

  1. 移除_pages里面對應(yīng)的數(shù)據(jù)。
  2. 將appendLoadId++,并且通過appendLoadIdCh發(fā)送出去啸盏。

??那么哪里在消費appendLoadId事件呢重贺?這就要回到加載更多的地方,當(dāng)時提到了consumeAppendGenerationIdAsFlow方法宫补。是的檬姥,就是這個方法對外提供消費入口:

    @OptIn(ExperimentalCoroutinesApi::class)
    fun consumeAppendGenerationIdAsFlow(): Flow<Int> {
        return appendLoadIdCh.consumeAsFlow()
            .onStart { appendLoadIdCh.offer(appendLoadId) }
    }

??從這里,我們便知道前面在collectAsGenerationalViewportHints方法里面粉怕,為啥在generationId(即generationId)不為0時健民,需要丟棄一個數(shù)據(jù)了。不為0表示在進(jìn)行Drop贫贝,如果PREPENDAPPEND同時進(jìn)行加載秉犹,并且同時Drop,可能會導(dǎo)致死循環(huán)稚晚,所以需要跳過一個崇堵,讓任意一個加載成功,另一個加載失敗(因為會Drop)客燕。
??如上便是Drop的所有內(nèi)容鸳劳。

7. 總結(jié)

??到此,上篇的內(nèi)容到此結(jié)束也搓,在這里赏廓,我對本文內(nèi)容做一個簡單的總結(jié)。

  1. Paging3相比于Paging2傍妒,PagingSource(即Paging2的DataSource)Api簡潔了許多幔摸,使用起來也方便多了。
  2. 整個Paing3可以分為兩層颤练,分別是:數(shù)據(jù)請求層和Ui層既忆。兩個層之間通過Flow連接起來的。
  3. Refresh對于數(shù)據(jù)請求層來說,是一個主動的過程患雇,主要是通過PageFetcherSnapshotdoInitialLoad方法進(jìn)行請求的跃脊。數(shù)據(jù)請求的基本過程如下:請求前,更新對應(yīng)LoadType的LoadState苛吱,并且同步到Ui層匾乓;其次,通過調(diào)用PagingSourceload方法獲取一個Load.Result對象又谋;然后隘道,根據(jù)Load.Result的類型進(jìn)行不同的操作登颓,如果是Load.Page對象移怯,主要是過程是神年,更新對應(yīng)LoadType的LoadState薪前,將數(shù)據(jù)添加到PageFetcherSnapshotState里面褒翰,同時發(fā)送一個PageEvent到Ui層苍狰。
  4. Append對于數(shù)據(jù)請求層來說是一個被動的過程露筒,由UI層觸發(fā)耻涛。主要是UiReceiver作為橋梁進(jìn)行請求废酷,最終會調(diào)用PageFetcherSnapshotdoLoad方法。請求的過程跟Refresh類似抹缕,只不過這個過程多了Drop操作澈蟆,Drop主要是跟PagingConfig里面的maxSize有關(guān)。

??下篇我將分析RemoteMediator卓研,敬請期待趴俘。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市奏赘,隨后出現(xiàn)的幾起案子寥闪,更是在濱河造成了極大的恐慌,老刑警劉巖磨淌,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疲憋,死亡現(xiàn)場離奇詭異,居然都是意外死亡梁只,警方通過查閱死者的電腦和手機缚柳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來敛纲,“玉大人喂击,你說我怎么就攤上這事∮傧瑁” “怎么了翰绊?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我监嗜,道長谐檀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任裁奇,我火速辦了婚禮桐猬,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘刽肠。我一直安慰自己溃肪,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布音五。 她就那樣靜靜地躺著惫撰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪躺涝。 梳的紋絲不亂的頭發(fā)上厨钻,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音坚嗜,去河邊找鬼夯膀。 笑死,一個胖子當(dāng)著我的面吹牛苍蔬,可吹牛的內(nèi)容都是我干的诱建。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼银室,長吁一口氣:“原來是場噩夢啊……” “哼涂佃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蜈敢,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤辜荠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抓狭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體伯病,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年否过,在試婚紗的時候發(fā)現(xiàn)自己被綠了午笛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡苗桂,死狀恐怖药磺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情煤伟,我是刑警寧澤癌佩,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布木缝,位于F島的核電站,受9級特大地震影響围辙,放射性物質(zhì)發(fā)生泄漏我碟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一姚建、第九天 我趴在偏房一處隱蔽的房頂上張望矫俺。 院中可真熱鬧,春花似錦掸冤、人聲如沸厘托。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽催烘。三九已至,卻和暖如春缎罢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背考杉。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工策精, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人崇棠。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓咽袜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親枕稀。 傳聞我的和親對象是個殘疾皇子询刹,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容

  • 技術(shù)不止,文章有料萎坷,加 JiuXinDev 入群凹联,Android 搬磚路上不孤單 前言 又到了學(xué)習(xí) Android...
    九心_閱讀 12,187評論 5 19
  • ??距離上一篇Jetpack源碼分析的文章已經(jīng)兩個月,時間間隔確實有點長哆档。最近蔽挠,感覺自己的學(xué)習(xí)積極性不那么的高,看...
    瓊珶和予閱讀 1,885評論 2 4
  • 夜鶯2517閱讀 127,712評論 1 9
  • 版本:ios 1.2.1 亮點: 1.app角標(biāo)可以實時更新天氣溫度或選擇空氣質(zhì)量瓜浸,建議處女座就不要選了澳淑,不然老想...
    我就是沉沉閱讀 6,878評論 1 6
  • 我是一名過去式的高三狗,很可悲插佛,在這三年里我沒有戀愛杠巡,看著同齡的小伙伴們一對兒一對兒的,我的心不好受雇寇。怎么說呢氢拥,高...
    小娘紙閱讀 3,375評論 4 7