高效管理大型資料集是行動 App 效能的基石。分頁(Pagination)能確保使用者在面對大量內容時,依然享有流暢的互動體驗。前面章節已探討過各種 App 內部的分頁實作,本章把焦點拉高一層,挑戰一個更廣泛的題目:設計一個可重複使用的分頁函式庫(Pagination library)。

一個設計良好的分頁函式庫能為成長中的組織帶來顯著效益:

  • 一致性:跨 App 提供統一的行為與 API。
  • 減少重複:避免每個團隊重新實作相同邏輯。
  • 集中優化:效能改善一次,所有使用者受惠。

設計函式庫與設計應用程式有本質上的差異。App 透過介面與畫面服務終端使用者,而函式庫則透過程式碼服務開發者。因此我們的設計必須以開發者如何整合、如何使用為優先考量。

設計函式庫時,「使用者體驗」的定義會轉為:開發者採用我們方案的容易程度、整合到專案中的順暢度,以及能否確實解決他們的分頁需求。

Step 1:釐清問題與設定設計範圍#

在進入設計細節之前,我們必須先釐清「要建什麼」與「為什麼要建」。以下是典型面試情境中的對話範例。

本章假設讀者已熟悉 offset-based 與 cursor-based 等分頁技巧。若需複習,可參閱第 10 章「Mobile System Design Building Blocks」中的 Pagination 章節。

需求對話#

Candidate: 我想先確認核心需求。這個函式庫應該要支援常見的分頁模式,例如 offset 與 cursor-based,並允許從本地或遠端資料來源進行分頁。這個方向符合期待嗎?

Interviewer: 是的,這就是我們期望的彈性與範圍,而且要能處理任何類型的應用程式資料。

Candidate: 了解。函式庫需要包含渲染分頁資料的 UI 元件,還是只專注在分頁邏輯與資料管理?

Interviewer: 這次我們只聚焦在商業邏輯,排除 UI 元件。

Candidate: 為了優化效能,我會加入快取系統來儲存已抓取的頁面。預設使用 in-memory 快取以便快速存取,並可選擇啟用 disk-based 快取以跨 session 持久化資料。你覺得如何?

Interviewer: 很完美。

Candidate: 我也建議加入 prefetching(預先抓取)能力,預測使用者導覽模式。例如使用者瀏覽第 5 頁時,函式庫自動在背景載入第 6、7 頁,降低感知延遲。要不要也支援快速滾動(fast scrolling)場景?

Interviewer: prefetching 加入,但不包含 fast scrolling,因為我們不處理 UI。

Candidate: 最後,這個函式庫會開源嗎?這會影響 API 設計、文件標準與版本策略。

Interviewer: 是的,我們打算開源。

Requirements#

由以上討論,我們的分頁函式庫須滿足以下功能性需求(functional requirements)

  • 支援多種應用程式資料型別與多種分頁技巧。
  • 能針對不同的本地與遠端資料來源進行分頁。
  • 只提供商業邏輯功能,不提供 UI 支援。
  • 提供 in-memory 快取與可選的 disk 快取。
  • 允許使用者預先抓取特定頁面。

非功能性需求(non-functional requirements)

  • Performance:提供高效分頁,資源使用最佳化,可安全運作於多執行緒環境。
  • Reliability:優雅處理網路失敗與中斷,確保跨裝置與 OS 版本的一致行為。
  • Usability:提供直觀的 API、完整文件與學習資源,協助開發者正確整合分頁功能。

超出範圍(out of scope)

  • UI 層的分頁支援。
  • Fast scrolling 支援。

Step 2:API 設計#

確立需求後,我們來設計函式庫的公開 API(public API)。這個介面必須直觀、有彈性,並穩健到能滿足行動開發者的多樣需求。

函式庫的 API 就是它的公開門面——所有開發者能直接互動的 function、class、method 與 property。正如優秀的 UI 讓 App 對使用者直觀,優秀的 API 也讓函式庫對開發者親和。

好的 public API 具備三個關鍵特質:

  • 清楚的文件:幫助開發者理解每個元件的用法。
  • 穩定性:讓開發者依賴其一致行為。
  • 符合直覺的設計:遵循平台慣例與模式。

此設計的核心是 Paginator 介面,作為使用者與函式庫互動的基石。

設計函式庫 API 時,先聚焦核心元素:開發者最常互動的關鍵 interface、class 與 data model。這些是 API 表面的基礎,會形塑整個使用方式。

Paginator#

行動 App 常處理多樣化的資料集,例如社群動態、聊天紀錄、商品目錄等。為支援任意資料型別,我們使用 generics 定義 Paginator<T>,帶來以下效益:

  • Flexibility:單一 Paginator 可處理任意資料型別,避免重複程式碼。
  • Isolation:多個獨立實例可共存,各自管理獨立的快取與分頁流程。
  • Optimization:每個實例只追蹤相關資料,記憶體使用精簡。
  • Reliability:Generics 在編譯期強制型別安全,提早捕捉錯誤。

使用 Paginator<T> 泛型後,我們無需為 posts、messages 等各種內容建立獨立的 Paginator class,單一實作即可通用,大幅降低重複並提升可維護性。

Paginator<T> 介面需要以下核心方法對應需求:

  1. fetch:讓使用者請求資料來源中的特定頁面。
  2. fetchAround:預先抓取指定頁面前後的頁面。
  3. clear:清除快取中的特定頁面。

初版 API 定義如下:

interface Paginator<T> {
    fun fetch(key: String, pageSize: Int)

    fun fetchAround(
        key: String, pageSize: Int,
        depthLevel: Int, direction: PageFetchDirection
    )

    fun clear(key: String)
}

enum class PageFetchDirection { ALL, FORWARD, BACKWARD }
protocol Paginator {
    associatedtype T

    func fetch(key: String, pageSize: Int)

    func fetchAround(
        key: String, pageSize: Int,
        depthLevel: Int, direction: PageFetchDirection
    )

    func clear(key: String)
}

enum PageFetchDirection { case all, forward, backward }

Paginator 的方法皆為 idempotent,相同輸入呼叫多次會得到相同結果。

將 Paginator 定義為 interface/protocol 不只是作為函式庫的入口,更大幅提升可測試性——開發者可以用 test double 模擬各種情境。

支援多種分頁技巧#

此 API 的一項關鍵優勢是對分頁技巧保持中立。key 參數刻意設計成有彈性,能對應底層的分頁策略:

  • Offset-based paginationkey 可能是整數字串(例如 "1""2")。
  • Cursor-based paginationkey 可能是指向下一段資料集的 token(例如 "cursor123")。

如此一來,無論資料來源採用哪種分頁策略,開發者都能使用相同的 Paginator 介面。

資料傳遞(Data propagation)#

接著考慮函式庫如何回傳結果與錯誤。為保持輕量且無依賴,我們採用原生 callback,避免強迫開發者採用特定的 async 框架。

同時,每個方法都會回傳一個 PaginatorExecution 物件,讓使用者能在需要時取消操作。

因為是開源函式庫且會被多個 App 使用,必須對依賴保持謹慎。任何第三方依賴都會成為使用者專案的強制依賴。因此我們以原生語言構件實作核心功能,再透過擴充模組支援各種熱門框架(稍後會在 Deep dives 詳談)。

改良後的 Paginator 介面:

interface Paginator<T> {
    fun fetch(
        key: String, pageSize: Int,
        listener: PaginatorFetchListener<T>
    ): PaginatorExecution

    fun fetchAround(
        key: String,
        pageSize: Int,
        depthLevel: Int, direction: PageFetchDirection,
        listener: PaginatorFetchAroundListener<T>
    ): PaginatorExecution

    fun clear(key: String)
}
protocol Paginator {
    associatedtype T

    func fetch(
        key: String, pageSize: Int,
        listener: PaginatorFetchListener<T>
    ) -> PaginatorExecution

    func fetchAround(
        key: String, pageSize: Int,
        depthLevel: Int, direction: PageFetchDirection,
        listener: PaginatorFetchAroundListener<T>
    ) -> PaginatorExecution

    func clear(key: String)
}

內部上,每個 Paginator 實例維護一個 thread-safe 的快取(通常是 Map),將 page key 對應到資料,加速最近載入頁面的存取。

資料模型(Data models)#

核心資料模型是 PaginatorPage,封裝表達單一頁面所需的一切:

public class PaginatorPage<T> {
    val content: List<T>
    val key: String
    val expirationTime: String?
    val prevKey: String?
    val nextKey: String?
}
public class PaginatorPage<T> {
    let content: [T]
    let key: String
    let expirationTime: String?
    let prevKey: String?
    let nextKey: String?
}

PaginatorPage 既包含分頁內容,也包含導覽所需的 metadata:當前 key、前後 key(若有)、以及可選的 expirationTime,讓函式庫能判斷快取何時需要更新。

這裡刻意使用 public class 而非前幾章常見的 data class 或 struct,這對函式庫開發至關重要。

  • Kotlin data class:自動生成 copycomponentNequalshashCode 等方法。便利歸便利,但修改屬性時會連帶改變生成的方法,造成 binary compatibility 問題。
  • Swift struct:值型別的記憶體配置屬於其 binary interface,修改儲存屬性會影響 layout,進而衝擊 ABI 穩定性。

使用 class 能更好地控制公開介面,確保跨版本相容性。

App 如何實作 Paginator 介面#

開發者將分頁函式庫整合進 App 時,通常在 repository 或 UI state holder 中使用 Paginator:

  • 透過 CallbackPaginator 實作建立設定好的 Paginator 實例(下節詳述)。
  • 在特定使用者動作(如滾動)時觸發 Paginator 方法。
  • 將 PaginatorPage 物件組合為統一清單。
  • 監控分頁狀態,決定何時載入更多資料。
  • 處理各種狀態:loading、error、empty。

對於線性分頁場景(例如滾動瀏覽),App 主要使用 fetch 依序載入頁面;非線性場景則使用 fetchAround 預抓目標頁面周邊的頁面,確保流暢導覽。

這種職責劃分讓 Paginator 專注於高效的資料擷取與快取,而 App 端程式碼負責呈現邏輯與使用者互動。

建立可配置的 Paginator 實例#

雖然 Paginator 介面定義了分頁的契約,但 App 需要可以針對自身情境設定的具體實作。我們的函式庫提供 CallbackPaginator<T>,它透過 dependency injection 允許高度客製。

CallbackPaginator 由以下元件組成:

  1. PaginatorDataSource<T>:實際的資料擷取來源(遠端 API、本地 DB 或混合)。
  2. PageValidator<T>:驗證快取頁面是否仍有效(檢查 expiration timestamp、資料完整性等)。若驗證失敗,觸發重新抓取。
  3. PaginatorStore<T>(可選):on-disk 儲存,提供離線使用並減少冗餘網路呼叫。
  4. PaginatorConfig:控制併發處理、快取淘汰策略等設定。

CallbackPaginator 的定義:

class CallbackPaginator<T>(
    dataSource: PaginatorDataSource<T>,
    validator: PageValidator<T>,
    store: PaginatorStore<T>? = null,
    config: PaginatorConfig = DefaultPaginatorConfig(),
) : Paginator<T> { ... }

class PaginatorConfig(
    val threadPool: ExecutorService,
    val maxParallelCalls: Int? = null,
    val inMemoryEvictionStrategy: PaginatorEvictionStrategy,
    val storeEvictionStrategy: PaginatorEvictionStrategy? = null,
    // ...
)
class CallbackPaginator<T>: Paginator<T> {
    init(
        dataSource: PaginatorDataSource<T>,
        validator: PageValidator<T>,
        store: PaginatorStore<T>? = nil,
        config: PaginatorConfig = DefaultPaginatorConfig()
    ) {
        // ...
    }
}

有些函式庫採用 static 設定方法(例如 Paginator.init(...)),但這不適合我們的情況。每個 Paginator 實例需要獨立且高度客製的設定,因此採用 instance-level 設定。

函式庫內建常見的淘汰策略:LRU(Least Recently Used)、FIFO(First In, First Out)、LIFO(Last In, First Out)。此外開發者也能依需求自訂策略,例如 TTL、記憶體大小上限或頁面數上限。

Step 3:高階 Client 架構#

API 設計完成後,我們來描繪高階 client 架構。這個結構必須強調模組化、可擴展性與可維護性,才能順利整合進各種行動 App。

下圖概覽了核心元件與互動關係:實線箭頭代表資料流,虛線箭頭代表依賴,指向框外的箭頭代表對外通訊。灰底元件為開發者提供的實作,其他則由函式庫提供。

Figure 1: Pagination library high-level architecture

除了先前已介紹的元件外,還有兩個關鍵元件能提升 Paginator 的效率與聚焦:

  • PaginatorScheduler:負責請求相關的所有操作——佇列、排序、處理回應,並管理 debouncing、throttling、retry 與 concurrency。將這些職責從 Paginator 分離,可保持其單一職責。
  • PaginatorEvictor:管理 in-memory 與 disk 快取的淘汰,使不同策略可獨立實作,不影響系統其他部分。

將功能拆分為專屬元件有兩個好處:(1) 展現對模組化設計原則的理解;(2) 清楚對應到面試官關心的需求,如快取與 prefetching。

這個設計在彈性與封裝之間取得平衡:使用者可以整合自訂資料來源與儲存方案,而函式庫負責處理排程與快取管理的複雜細節。

Step 4:Deep dives#

在確立高階架構後,讓我們深入探討幾個關鍵實作細節:

  • 有效的快取策略
  • API 呼叫的優先順序
  • 模組化 API 設計
  • 失敗處理與版本管理

有效的快取策略#

快取是分頁函式庫的樞紐,直接影響效能與使用者體驗。我們的快取系統結合了 in-memorydisk-based 兩個層級。

使用者可選擇啟用 PaginatorStore 在磁碟上持久化 PaginatorPage 物件;而 in-memory 快取則幾乎必備。兩種快取都能透過 PaginatorConfig 中的淘汰策略細調。

In-memory 快取#

核心是 PaginatorInMemoryCache,為分頁資料提供快速存取。其結構為 Map<String, PaginatorPageResult<T>>

  • key:String,頁面唯一識別(offset、page index 或 cursor)。
  • valuePaginatorPageResult<T>,代表頁面當前生命週期狀態:
    • key 不存在:頁面尚待初次抓取。
    • Loading:正在擷取中。
    • Success(page):頁面就緒且已驗證。
    • Error(message):抓取失敗,附上診斷資訊。

PaginatorPageResult 顯式建模這些狀態的好處:

  • 編譯期的型別安全。
  • 清楚表達頁面狀態。
  • 方便除錯。
  • 隨程式碼成長仍保有良好擴展性。

雙層快取如何運作#

Figure 2: Paginator with cache misses data flow

當使用者呼叫 Paginator 的 fetch(step 0),系統依序執行以下流程:

  1. In-memory 快取檢查:先查詢 in-memory cache,命中則立即回傳。
  2. Disk 快取檢查(可選):若 in-memory 未命中且 PaginatorStore 已設定,則查磁碟快取。命中則提升到 memory 並回傳。
  3. Data source 擷取:兩層都未命中時,由 PaginatorScheduler 觸發 PaginatorDataSource 的抓取。
  4. Validator 檢查:新頁面抓到後,PageValidator 驗證其有效性(格式、過期時間等)。
  5. 更新快取:通過驗證後,同時更新 in-memory 與 disk 快取。
  6. 回傳頁面給呼叫端。

這個分層設計兼顧速度(in-memory)與持久性(disk)。PaginatorEvictor 負責執行淘汰策略,保持快取精簡。

效益與注意事項#

效益

  • 透過避免網路呼叫降低延遲,提升效能。
  • 智慧請求管理降低網路用量。
  • 更平順的使用者體驗。

注意事項

  • 系統假設 cache key 直接對應網路請求。若非如此,需實作自訂 key 生成邏輯。
  • Paginator 引擎在多執行緒環境運作,必須確保 thread-safe 存取與 atomic 操作,避免 race condition 與資料損毀。

此外可在函式庫中設定 analytics 與 logger provider,追蹤 cache hit/miss/eviction,協助開發者依情境調校。

業界實作參考

  • Kotlin Multiplatform 的 Store5 函式庫與 iOS 的 Hyperoslo Cache 函式庫都能同時寫入 in-memory 與 disk,提供快速讀取與持久備援。
  • Google 的 Jetpack Paging 鼓勵 network + database 混合模式:以本地 DB 作為分頁內容的 single source of truth,由 RemoteMediator 僅在本地資料耗盡時抓取新頁面。

API 呼叫的優先順序#

使用者期待快速回應,但函式庫必須同時處理多個任務,且常受限於裝置資源與網路狀況。當出現以下情境時,優先順序就格外重要:

  • 併發請求爭奪頻寬或運算能力。
  • 背景操作(如 prefetching)與使用者觸發的操作重疊。
  • 電量低或網路不佳時需要最佳化效能。

例如使用者滾動觸發的載入應優先於背景 prefetch;fetchAround 中,被明確請求的頁面應優先於周邊頁面。為此,我們在公開 API 中加入 priority 控制。

改良後的介面:

interface Paginator<T> {
    fun fetch(
        key: String,
        pageSize: Int,
        listener: PaginatorFetchListener<T>,
        priority: FetchPriority = FetchPriority.MEDIUM
    ): PaginatorExecution

    fun fetchAround(
        key: String, pageSize: Int,
        depthLevel: Int, direction: PageFetchDirection,
        listener: PaginatorFetchAroundListener<T>,
        keyPriority: FetchPriority = FetchPriority.MEDIUM,
        aroundPriority: FetchPriority = FetchPriority.LOW
    ): PaginatorExecution
}

enum class FetchPriority { HIGH, MEDIUM, LOW }
protocol Paginator {
    associatedtype T

    func fetch(
        key: String, pageSize: Int,
        listener: PaginatorFetchListener<T>,
        priority: FetchPriority
    ) -> PaginatorExecution

    func fetchAround(
        key: String, pageSize: Int,
        depthLevel: Int, direction: PageFetchDirection,
        listener: PaginatorFetchAroundListener<T>,
        keyPriority: FetchPriority,
        aroundPriority: FetchPriority
    ) -> PaginatorExecution
}

enum FetchPriority { case high, medium, low }

針對 fetchAround,我們引入 keyPriority(主要頁面)與 aroundPriority(周邊頁面)兩個層級,提供更細緻的控制。

基於優先順序的任務排程#

實作 priority 需要一個穩健的排程策略。下表比較幾種方案:

方案說明優點缺點
每個優先級獨立 queue每個優先級一個 queue,依序處理實作簡單直覺、職責清楚優先級增加後效能下降、跨 queue 工作量平衡複雜、記憶體開銷高
Priority Queue單一 queue,透過 heap 自動維持優先順序高效的優先級排序、架構單純、記憶體占用低Heap 重排有開銷、無公平性保證,低優先級可能飢餓
多 ThreadPool 分層各優先級獨立 thread pool,高優先級資源多真正併發、硬體平行、資源利用好Thread pool 管理複雜、資源需求高
Weighted Round-Robin依權重循環處理,高優先級獲得更多時間所有優先級都有保障、執行可預期、公平性好權重調校複雜、高優先級可能被延遲

我們選擇 Priority Queue 模式:它有效排序、確保高優先級任務先執行、實作單純,很適合行動裝置的資源限制。

業界實作參考:Google 的 Volley 網路函式庫允許開發者為 request 標記 Priority,讓「immediate」請求在 queue 中插隊。

模組化 API 設計#

Async 框架相容層#

設計行動函式庫時面臨一個關鍵抉擇:如何有效管理非同步操作。Kotlin Coroutines、Swift Combine 等框架各有強大方案,但若強制函式庫綁定某一種,就會對使用不同技術棧的使用者造成負擔——想像一個 RxJava App 為了使用我們的函式庫被迫引入 Coroutines。

平台實作細節

  • Android:傳統 callback、Kotlin Coroutines 與 Flow、RxJava、LiveData、CompletableFuture
  • iOS:closure-based completion handler、Combine、async/await、delegate pattern、Grand Central Dispatch (GCD)。

我們的方案:以 native callback 為核心,不依賴任何外部框架;再以選用的擴充模組銜接熱門框架。

  • paginator-core:核心功能,無外部依賴。
  • paginator-coroutinespaginator-combinepaginator-rx:針對特定 async 方案的最佳化支援。

此策略的效益:

  • Flexibility:開發者只引入符合技術棧的擴充。
  • 輕量核心:base 模組不被外部框架拖累。
  • 原生整合:每個擴充遵循對應框架的慣例。

平台實作細節

  • Android:Gradle 原生支援從單一 codebase 發布多個 library artifact,每個 async 整合可為獨立 artifact。
  • iOS:傳統上以單一 framework 發布,但可透過以下方式達到模組化:
    1. Conditional compilation 選擇性包含程式碼。
    2. 為不同 async 模式建立擴充檔案,讓開發者依需求加入。
    3. 使用 Swift Package Manager 定義獨立 product 或 target。

Kotlin Coroutines 與 Swift async/await 的變體範例:

interface Paginator<T> {
    suspend fun fetch(key: String, pageSize: Int): PaginatorPage<T>

    fun fetchAround(
        key: String, pageSize: Int,
        depthLevel: Int, direction: PageFetchDirection
    ): Flow<PaginatorPage<T>>

    suspend fun clear(key: String)
}
protocol Paginator {
    associatedtype T

    func fetch(
        key: String, pageSize: Int
    ) async throws -> PaginatorPage<T>

    func fetchAround(
        key: String, pageSize: Int,
        depthLevel: Int, direction: PageFetchDirection
    ) -> AsyncThrowingStream<PaginatorPage<T>, Error>

    func clear(key: String) async throws
}

分頁函式庫測試套件#

除了 async 彈性,測試支援同樣重要。我們的介面基礎本已利於測試,更可進一步提供專屬的 testing-only artifact,內含預建的 test double 與工具。

此 testing artifact 提供:

  • Pre-built test doubles:模擬 PaginatorDataSourcePaginatorStorePageValidator,免除手動撰寫 mock。
  • Helper functions:產生示範分頁資料、模擬網路延遲。
  • 錯誤模擬:測試錯誤處理與復原邏輯。

對開發者的好處:

  • 無需自行維護客製 test 實作。
  • Test double 自動與函式庫更新同步。
  • 讓邊界案例的測試變簡單,提升整體覆蓋率。
  • 體現我們對測試最佳實踐的重視。

將 testing artifact 獨立出來既保持核心函式庫的輕量,也讓測試工具能獨立演進,而開發者的 production build 不會被測試程式碼膨脹。

業界實作參考

  • Google 的 Jetpack Paging 3 提供 PagingData 串流,可輸出為 LiveData、RxJava Flowable 或 Kotlin Flow 依 App 所需。
  • iOS 的 Moya 網路函式庫一開始就為測試設計,允許提供 stub response 或為每個 endpoint 設定 sampleData,讓單元測試無需真實網路呼叫。

失敗處理與版本管理#

失敗點處理#

分頁函式庫處於資料來源、網路與裝置限制的交界,容易遇到各種失敗。以下逐一列出關鍵失敗點與對應策略。

網路失敗#

問題:timeout、連線中斷、伺服器錯誤等都可能中斷分頁。

Mitigation:採用 retry 配合 exponential backoff 處理暫時性失敗。失敗時透過 callback 傳遞詳細錯誤,讓開發者選擇退而回到快取資料或顯示友善訊息。

資料庫錯誤#

問題:本地資料來源可能發生 query 失敗或資料損壞。

MitigationPaginatorDataSource 內建錯誤處理,透過 callback 回報。開發者可設定 fallback,例如載入舊的快取資料。

快取不一致#

問題:過期或損壞的 cache entry 可能導致呈現錯誤內容。

MitigationPageValidator 使用 expiration time 或 checksum 驗證頁面。失效則觸發快取作廢與重新抓取,搭配 PaginatorConfig 的淘汰策略保持快取精實可靠。

併發問題#

問題:多執行緒環境下存取快取或同時抓取可能引發 race condition。

Mitigation:使用 thread-safe 資料結構(例如 concurrent map)與同步機制,保障 in-memory cache 與 PaginatorStore 的跨執行緒一致性。

無效的 page key#

問題:在 cursor-based 或 keyset pagination 中,使用者可能請求過期或格式錯誤的 key。

Mitigation:處理前先驗證 key,透過 callback 回傳明確錯誤訊息,引導開發者重置到已知起點。

小結#

這些 mitigation 的設計以使用者體驗為先——失敗不會讓 App 崩潰,而是優雅降級。在 PaginatorConfig 中加入 analytics 與 logging 選項,可進一步協助線上監控與除錯。

版本管理#

作為開源函式庫,清晰的版本策略對開發者信任至關重要。我們採用 Semantic Versioning(SemVer)MAJOR.MINOR.PATCH 格式:

  • MAJOR(X.y.z):破壞性變更(例如修改 Paginator 介面、變更預設快取行為)。
  • MINOR(x.Y.z):新增功能並維持相容(例如擴充 PaginatorConfig)。
  • PATCH(x.y.Z):bug 修復與小改動,不改變行為。

平台實作細節:iOS 與 Android 生態都偏好 SemVer:

  • iOS 的 Swift Package Manager (SPM) 以 SemVer 作為依賴解析基礎。
  • Android 的 GradleMaven 也依賴 SemVer 自動解析版本衝突。

不遵循 SemVer 將失去這些工具提供的完善依賴管理能力。

其他版本策略#

雖然 SemVer 最廣泛,但某些生態也採用 Calendar Versioning(CalVer)。例如 Jetpack Compose 結合兩者管理其快速演進的 UI toolkit:

  • 個別 Compose 函式庫(compose-uicompose-materialcompose-foundation)嚴格採用 SemVer。
  • Compose 另提供 Bill of Materials (BOM) artifact,採用 CalVer 格式 YYYY.MM.PATCH(例如 2025.02.00)。

Compose BOM 將特定 CalVer 版本對應到一組相容的函式庫版本(例如 compose.animation:1.7.8compose.material3:1.3.1),讓使用者以單一日曆版號取得一組保證相容的函式庫組合。

Step 5:總結#

本章設計了一個具彈性的分頁函式庫,具備以下能力:

  • 支援任意 App 資料型別。
  • 支援 offset-based、cursor-based 等多種分頁技巧。
  • 同時支援本地與遠端資料來源。

我們定義了 public API、設計了穩健且可擴充的架構,並涵蓋了快取、API 優先順序、失敗點、測試與版本管理等關鍵議題。

若面試還有時間或想挑戰更進階的需求,可繼續探討:

  • UI 層分頁支援:整合 table view、collection view 等滾動元件。
  • Fast scrolling:快速跳過大型資料集的區段。
  • 背景服務:App 閒置或在背景時預抓頁面。
  • 自訂錯誤處理:讓使用者定義 retry、fallback、使用者提示等策略。
  • 部分頁面載入:在資源有限或只需部分資料時最佳化效能。