在本程式碼研究室中,您將使用離線快取(offline caching)功能來改善 app 的使用者體驗。許多 app 都依賴來自網路的 data。如果 app 在每次啟動時都從 server 擷取 data,並顯示 loading 畫面,可能會造成使用者體驗不佳,導致使用者解除安裝 app。
使用者啟動 app 時,會希望 app 能快速顯示 data。實作離線快取(offline caching)功能就能實現這個目標。離線快取(offline caching)是指 app 將從網路擷取的 data 儲存到裝置的本機儲存空間(local storage),進而加快存取速度。
由於 app 將可從網路取得 data,並且保留先前下載結果的離線快取(offline cache),因此您需要讓 app 透過某種方式彙整來自多個來源的 data。做法是實作repository class,做為 app data 的單一可靠資料來源,並從 view model 中提取資料來源 (例如網路(network)、快取(cache)等)。
學習目標
- 如何實作 repository,以便從 app 的其他部分提取 data layer。
- 如何使用 repository 載入快取資料(cached data)。
範例程式碼
下載專案程式碼
請注意,資料夾名稱是 RepositoryPattern-Starter
。在 Android Studio 中開啟專案時,請選取這個資料夾。
範例程式碼網址:
https://github.com/google-developer-training/android-kotlin-fundamentals-starter-apps/tree/master/RepositoryPattern-Starter
分支版本名稱:master
範例應用程式總覽
DevBytes app 會在 RecyclerView
中顯示 Android 開發人員 YouTube 頻道的 DevBytes 影片清單(list),使用者可以從中點選以開啟影片的連結。

雖然範例程式碼可以完全正常運作,但有重大瑕疵,可能會對使用者體驗造成負面影響。如果使用者的網路連線不穩,或根本沒有網路連線,系統將無法顯示任何一部影片。即使先前已開啟 app 也是如此。假如使用者退出 app 並重新啟動,這次不使用網際網路,app 會嘗試重新下載影片清單(list),但沒有成功。
您可以在模擬器中查看實際運作情形。
- 在 Android Emulator 中暫時開啟飛航模式 (依序點選「Settings App」>「Network & Internet」>「Airplane mode」)。
- 執行 DevBytes app,可觀察到畫面呈現空白。

- 務必先關閉飛航模式,再繼續完成程式碼研究室的其餘部分。
這是因為 DevBytes app 首次下載資料後,就不會快取任何資料以供日後使用。這個 app 目前包含 Room
database。您必須使用這個 database 來實作快取功能,並更新 view model 以使用「repository」,這樣一來,就能下載新資料或從 Room
database 中擷取資料。Repository class 會將這個邏輯從 view model 中提取出來,讓程式碼保持井然有序且已分離。
範例專案分為數個 packages。

除了歡迎您並鼓勵您熟悉程式碼以外,您只會接觸以下兩個檔案:repository/VideosRepository.kt
和 viewmodels/DevByteViewModel
。首先,您將建立 VideosRepository
class 來實作用於快取的 repository pattern (在接下來幾頁中會有更進一步的說明),接著更新 DevByteViewModel
以使用新的 VideosRepository
class。
不過在開始使用程式碼之前,請花點時間進一步瞭解快取(caching)和存放區模式(repository pattern)。
Caching 和 repository pattern
Repositories
存放區模式(repository pattern)是一種設計模式,可將資料層( data layer)與 app 的其他部分分開。資料層是指獨立於 UI 的 app 部分,用於處理 app 的資料和商業邏輯,讓 app 的其餘部分都能使用一致的 API 存取這類資料。儘管 UI 會向使用者顯示資訊,但資料層(data layer)會包括網路程式碼(networking code)、Room
database、錯誤處理(error handling),以及任何讀取或操控資料的程式碼。

Repository 可以解決資料來源(data sources) (例如 persistent models、web services 和 caches) 之間的衝突,並集中管理這項資料的變更。下圖顯示 app components (例如 activity) 如何透過 repository 與資料來源(data sources)互動。

如要實作 repository,請使用其他 class,例如您在下一個工作中建立的 VideosRepository
class。Repository class 可將資料來源(data sources)與 app 的其他部分分開,並提供簡潔的 API,方便存取 app 其餘部分的資料。使用 repository class 可確保這個程式碼與 ViewModel
class 分開,而且是適合用於程式碼分隔和架構的建議最佳做法。
使用 repository 的優點
repository module 會處理資料作業,並且讓您可以使用多個後端。在一般的實際 app 中,repository 會實作邏輯,以判斷是否要從網路擷取資料,或使用本機資料庫(local database)中的快取結果。透過 repository,您可以替換實作的詳細資料(details),例如遷移至不同的 persistence library,而不會影響到呼叫的程式碼 (例如 view model)。這也有助於讓程式碼模組化(modular)且可用於測試(testable)。您可以輕鬆模擬 repository,並測試程式碼的其他部分。
Repository 應做為 app 特定資料的單一可靠資料來源。使用網路資源(networked resource)和離線快取(offline cache)等多個資料來源時,repository 能夠盡可能地確保 app 資料正確無誤且為最新狀態,即使 app 處於離線狀態,也能提供最佳體驗。
Caching
快取(cache)是指 app 所使用的資料儲存空間。舉例來說,使用者網路連線中斷時,您可能會想要暫時儲存網路的資料。即使已無法使用網路,app 仍可借助快取資料。快取也可以為不再顯示於畫面上的 activity 儲存暫存資料,甚至儲存 app 啟動期間的持續性資料(persisting data)。
快取(cache)可以採用多種形式,有些較為簡單、有些較為複雜,視特定工作而定。以下表格說明在 Android 中實作網路快取(network caching)的方法。
快取技術 | 用途 |
---|---|
Retrofit 是一個 networking library,用於實作 Android 適用的 type-safe REST client。您可以設定 Retrofit 在本機儲存所有網路結果的副本。 | 對於簡單的 requests 和 responses、網路呼叫(network calls)頻率不高或小型資料集(small datasets)來說,這是不錯的解決方式。 |
您可以使用 DataStore 儲存 key-value 組合。 | 如果 key 很少且 value 較為簡單 (例如 app 設定),這是不錯的解決方式。您無法使用這項技術儲存大量結構化資料(structured data)。 |
您可以存取 app 的內部儲存空間目錄,並將資料檔案儲存在其中。app 的 package name 會指定 app 的內部儲存空間目錄(internal storage directory),這個目錄位於 Android 檔案系統中的特殊位置。目錄僅供您的 app 使用,而且會在 app 解除安裝後清除。 | 如果有檔案系統可以解決的特定需求 (例如您需要儲存媒體檔案或資料檔案,且必須自行管理檔案時),這是不錯的解決方案。您無法使用這項技術儲存 app 所需查詢的複雜結構化資料(structured data)。 |
您可以使用 Room 快取資料(cache data)。Room 是一個 SQLite object-mapping library,可提供以 SQLite 為基礎的抽象層(abstraction layer)。 | 對於複雜的結構化可查詢資料,這是建議的解決方式,因為在裝置的檔案系統中儲存結構化資料(structured data)的最佳方式就是儲存在 local SQLite database。 |
在這個程式碼研究室中,您將使用 Room
,因為這是在裝置的檔案系統中儲存結構化資料(structured data)的建議方式。DevBytes app 已設定為使用 Room
。您的工作是使用 repository pattern 實作離線快取(offline caching),將資料層(data layer)與 UI code 分開。
實作 VideoRepository
工作:建立 repository
在這項工作中,您會建立 repository 來管理在上一個工作中已實作的離線快取(offline caching)。Room database 沒有管理離線快取(offline caching)的邏輯,其中只有 insert、update、delete 及 retrieve 資料的方法。Repository 將使用邏輯擷取網路結果(network results),並讓 database 保持在最新狀態。
步驟 1:新增 repository
在 repository/VideosRepository.kt
建立 VideosRepository
class。傳入 VideosDatabase
object 做為 class 的 constructor,以存取 DAO 方法。
1 | class VideosRepository(private val database: VideosDatabase) { |
- 在
VideosRepository
class 中,新增名為refreshVideos()
的suspend
方法,這個方法沒有引數,且不會 return 任何內容。這個方法是用於重新整理離線快取的 API。
1 | suspend fun refreshVideos() { |
注意:Android 中的 database 會儲存在檔案系統或磁碟中。如要儲存,這類 database 必須執行磁碟(disk) I/O 作業。Disk I/O 或讀取和寫入磁碟(disk)的速度緩慢,而且在操作完成之前會一律封鎖目前的 thread。因此,您必須在 I/O dispatcher中執行 disk I/O。這個 dispatcher 可用於使用 withContext(Dispatchers.IO) { … },將封鎖的 I/O 工作卸載至共用執行緒集區(shared pool of threads)。
- 在
refreshVideos()
方法中,將 coroutine context 切換為Dispatchers.IO
,以執行網路(network)和 database 作業。
1 | suspend fun refreshVideos() { |
- 在
withContext
區塊內,使用Retrofit
service instanceDevByteNetwork
從網路擷取DevByte
video playlist。
1 | val playlist = DevByteNetwork.devbytes.getPlaylist() |
- 在
refreshVideos()
方法中,從網路擷取 playlist 後,將 playlist 儲存在 Room database 中。如要儲存 playlist,請使用VideosDatabase
class。呼叫insertAll()
DAO 方法,傳入從網路擷取的 playlist。使用asDatabaseModel()
extension function,將 playlist 對應到 database object。
1 | database.videoDao.insertAll(playlist.asDatabaseModel()) |
- 以下是完整的
refreshVideos()
方法,其中包含追蹤何時該方法會被呼叫的 log statement:
1 | suspend fun refreshVideos() { |
步驟 2:從 database 擷取資料
在這個步驟中,您會建立 LiveData
object,以從 database 中讀取 video playlist。在 database 更新時,這個 LiveData
object 會自動更新。附加的 fragment 或 activity 會使用新的 value 重新整理。
注意:為求簡單,LiveData 會保留在這個範例中。一般來說,建議將 Flow 與 repositories 搭配使用,因為這種函式與生命週期無關。
- 在
VideosRepository
class 中,宣告名為videos
的LiveData
object 以存放DevByteVideo
object list。使用database.videoDao
初始化videos
object。呼叫getVideos()
DAO 方法。由於getVideos()
方法會 return database object list,而不是DevByteVideo
object list,因此 Android Studio 會 throw「類型不符(type mismatch)」的錯誤。
1 | val videos: LiveData<List<DevByteVideo>> = database.videoDao.getVideos() |
- 如要修正錯誤,請使用
Transformations.map
將 database object list 轉換成使用asDomainModel()
conversion function 的 domain object list。
1 | val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) { |
現在您已為 app 實作 repository。在下一個工作中,您將使用簡單的重新整理策略,以確保 local database 保持在最新狀態。
溫故知新:Transformations.map() 方法會使用 conversion function,將一個 LiveData object 轉換成另一個 LiveData object。只有在 active activity 和 fragment 觀察(observing)到 return 的 LiveData 屬性時,系統才會計算轉換(transformations)。
在 DevByteViewModel 中使用 VideoRepository
工作:使用 refresh 策略整合 repository
在這項工作中,您會使用簡單的重新整理(refresh)策略將 repository 與 ViewModel
整合。還會顯示 Room database 的 video playlist,而不是直接從網路擷取。
Database 重新整理是更新或重新整理 local database 的過程,讓 database 與網路中的資料保持同步。在這個範例 app 中,您將使用簡單的重新整理策略,其中向 repository 要求資料(requests data)的模組(module)會負責重新整理(refresh)本機資料(local data)。
在實際 app 中,這類策略可能會更複雜。例如,程式碼可能會自動在背景重新整理資料 (將頻寬納入考量),或快取使用者接下來最有可能使用的資料。
- 在
viewmodels/DevByteViewModel.kt
的DevByteViewModel
class 內,建立名為videosRepository
且 type 為VideosRepository
的 private member 變數。透過傳遞單例模式(singleton)VideosDatabase
object 將變數執行實例化(instantiate)。
1 | private val videosRepository = VideosRepository(getDatabase(application)) |
- 在
DevByteViewModel
class 中,將refreshDataFromNetwork()
方法替換為refreshDataFromRepository()
方法。舊方法refreshDataFromNetwork()
使用 Retrofit library 從網路擷取 video playlist。新方法則會從 repository 載入 video playlist。Repository 會決定要從哪個來源 (例如 network、database 等) 擷取 playlist,而不需在 view model 中包含實作的詳細資料(details)。Repository 也可以使程式碼更容易維護;即使日後要變更取得資料的實作方式,也不需要修改 view model。
1 | private fun refreshDataFromRepository() { |
- 在
DevByteViewModel
class 的init
區塊中,將函式呼叫從refreshDataFromNetwork()
變更為refreshDataFromRepository()
。這個程式碼可從 repository 擷取 video playlist,而不是直接從網路擷取。
1 | init { |
- 在
DevByteViewModel
class 中,刪除_playlist
屬性及其幕後(backing)屬性playlist
。
要刪除的程式碼
1 | private val _playlist = MutableLiveData<List<Video>>() |
- 在
DevByteViewModel
class 中,將videosRepository
object 執行實例化(instantiating)之後,新增一個名為playlist
的新val
,來保存 repository 中的LiveData
video list。
1 | val playlist = videosRepository.videos |
- 執行 app。 App 會照常運作,但系統現在會從網路擷取 DevBytes playlist,並儲存至 Room database。螢幕上顯示的 playlist 是取自 Room database,而非直接取自網路。

只要在模擬器或裝置上啟用飛航模式,就可以看出這項差異。
再次執行 app。請注意,app 不會顯示「Network Error」的 toast 訊息,而會顯示從離線快取(offline cache)中擷取的 playlist。
在模擬器或裝置上關閉飛航模式。
關閉再重新開啟 app。網路要求(network request)在背景(background)執行時,app 會從離線快取(offline cache)載入 playlist。
如果有來自網路的新資料,螢幕會自動更新以顯示新資料。不過,DevBytes server 不會重新整理其內容,因此您不會看到資料正在更新。
提示:如要移除測試用的快取(cache),最簡單的方法是解除安裝 app。
真厲害!在這個程式碼研究室中,您將 offline cache 與 ViewModel
整合,以顯示來自 repository 的 playlist,而不是從網路(network)擷取的 playlist。
總結
- 快取(Caching)是將從網路(network)擷取的資料儲存到裝置儲存空間(storage)的過程。快取(Caching)可讓 app 在裝置離線,或 app 必須重新存取相同資料時,得以存取資料。
- 如要讓 app 在裝置的檔案系統中儲存結構化資料(structured data),最好的做法是使用 local SQLite database。Room 是 SQLite object-mapping library,意味著其提供了以 SQLite 為基礎的抽象層(abstraction layer)。使用 Room 是實作離線快取(offline caching)的建議最佳做法。
- Repository class 可以將資料來源(data sources),例如 Room database 和 web services,與 app 的其他部分隔離。Repository class 提供簡潔的 API,方便存取 app 其餘部分的資料。
- 對於程式碼分隔(separation)和架構(architecture),使用 repositories 是建議的最佳做法。
- 設計離線快取(offline cache)時,最佳做法是將 app 的網路(network)、網域(domain)和 database objects 做出區隔。這項策略是區隔疑慮(separation of concerns)的例子之一。