存放區模式

1. 事前準備

說明

在本程式碼研究室中,您將使用離線快取功能來改善應用程式的使用者體驗。許多應用程式都依賴來自網路的資料。如果應用程式在每次啟動時都從伺服器擷取資料,並顯示載入畫面,可能會造成使用者體驗不佳,導致使用者解除安裝應用程式。

使用者啟動應用程式時,會希望應用程式能快速顯示資料。實作離線快取功能就能實現這個目標。離線快取是指應用程式將從網路擷取的資料儲存到裝置的本機儲存空間,進而加快存取速度。

由於應用程式將可從網路取得資料,並且保留先前下載結果的離線快取,因此您需要讓應用程式透過某種方式彙整來自多個來源的資料。做法是實作存放區類別,做為應用程式資料的單一可靠資料來源,並從檢視模型中提取資料來源 (例如網路、快取等)。

必備知識

您必須已經熟悉下列項目:

課程內容

  • 如何實作存放區,以便從應用程式的其他部分提取應用程式資料層。
  • 如何使用存放區載入快取資料。

課程步驟

  • 使用存放區來提取資料層,並將存放區類別與 ViewModel 整合。
  • 顯示離線快取的資料。

2. 範例程式碼

下載專案程式碼

請注意,資料夾名稱是 RepositoryPattern-Starter。在 Android Studio 中開啟專案時,請選取這個資料夾。

如要取得這個程式碼研究室的程式碼,並在 Android Studio 中開啟,請按照下列步驟操作:

取得程式碼

  1. 按一下上面顯示的網址。系統會在瀏覽器中開啟專案的 GitHub 頁面。
  2. 檢查並確認分支版本名稱與程式碼研究室中指定的版本相符。例如,在下列螢幕截圖中,分支版本名稱為「main」

8cf29fa81a862adb.png

  1. 在專案的 GitHub 頁面中,按一下「Code」按鈕,畫面上會出現彈出式視窗。

1debcf330fd04c7b.png

  1. 在彈出式視窗中,按一下「Download ZIP」按鈕,將專案儲存至電腦。等待下載作業完成。
  2. 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
  3. 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」視窗中,按一下「Open」

d8e9dbdeafe9038a.png

注意:如果 Android Studio 已開啟,請改為依序選取「File」>「Open」選單選項。

8d1fda7396afe8e5.png

  1. 在檔案瀏覽器中,前往已解壓縮的專案資料夾所在的位置 (可能位於「Downloads」資料夾中)。
  2. 按兩下該專案資料夾。
  3. 等待 Android Studio 開啟專案。
  4. 按一下「Run」按鈕 8de56cba7583251f.png 即可建構並執行應用程式。請確認應用程式的建構符合預期。

3. 範例應用程式總覽

DevBytes 應用程式會在 RecyclerView 中顯示 Android 開發人員 YouTube 頻道的 DevBytes 影片清單,使用者可以從中點選以開啟影片的連結

9757e53b89d2de7c.png

雖然範例程式碼可以完全正常運作,但有重大瑕疵,可能會對使用者體驗造成負面影響。如果使用者的網路連線不穩,或根本沒有網路連線,系統將無法顯示任何一部影片。即使先前已開啟應用程式也是如此。假如使用者退出應用程式並重新啟動,這次不使用網際網路,應用程式會嘗試重新下載影片清單,但沒有成功。

您可以在模擬器中查看實際運作情形。

  1. 在 Android Emulator 中暫時開啟飛航模式 (依序點選「Settings App」>「Network & Internet」>「Airplane mode」)。
  2. 執行 DevBytes 應用程式,可觀察到畫面呈現空白。

f0365b27d0dd8f78.png

  1. 務必先關閉飛航模式,再繼續完成程式碼研究室的其餘部分。

這是因為 DevBytes 應用程式首次下載資料後,就不會快取任何資料以供日後使用。這個應用程式目前包含 Room 資料庫。您必須使用這個資料庫來實作快取功能,並更新檢視模型以使用「存放區」,這樣一來,就能下載新資料或從 Room 資料庫中擷取資料。存放區類別會將這個邏輯從檢視模型中提取出來,讓程式碼保持井然有序且已分離。

範例專案分為數個套件。

25b5f8d0997df54c.png

除了歡迎您並鼓勵您熟悉程式碼以外,您只會接觸以下兩個檔案:repository/VideosRepository.ktviewmodels/DevByteViewModel。首先,您將建立 VideosRepository 類別來實作用於快取的存放區模式 (在接下來幾頁中會有更進一步的說明),接著更新 DevByteViewModel 以使用新的 VideosRepository 類別。

不過在開始使用程式碼之前,請花點時間進一步瞭解快取和存放區模式。

4. 快取與存放區模式

存放區

存放區模式是一種設計模式,可將資料層與應用程式的其他部分分開。資料層是指獨立於使用者介面的應用程式部分,用於處理應用程式的資料和商業邏輯,讓應用程式的其餘部分都能使用一致的 API 存取這類資料。儘管使用者介面會向使用者顯示資訊,但資料層會包括網路程式碼、Room 資料庫、錯誤處理,以及任何讀取或操控資料的程式碼。

9e528301efd49aea.png

存放區可以解決資料來源 (例如永久模型、網路服務和快取) 之間的衝突,並集中管理這項資料的變更。下圖顯示應用程式元件 (例如活動) 如何透過存放區與資料來源互動。

69021c8142d29198.png

如要實作存放區,請使用其他類別,例如您在下一個工作中建立的 VideosRepository 類別。存放區類別可將資料來源與應用程式的其他部分分開,並提供簡潔的 API,方便存取應用程式其餘部分的資料。使用存放區類別可確保這個程式碼與 ViewModel 類別分開,而且是適合用於程式碼分隔和架構的建議最佳做法。

使用存放區的優點

存放區模組會處理資料作業,並且讓您可以使用多個後端。在一般的實際應用程式中,存放區會實作邏輯,以判斷是否要從網路擷取資料,或使用本機資料庫中的快取結果。透過存放區,您可以替換實作的詳細資料,例如遷移至不同的持續性資料庫,而不會影響到呼叫的程式碼 (例如檢視模型)。這也有助於讓程式碼模組化且可用於測試。您可以輕鬆模擬存放區,並測試程式碼的其他部分。

存放區應做為應用程式特定資料的單一可靠資料來源。使用網路資源和離線快取等多個資料來源時,存放區能夠盡可能地確保應用程式資料正確無誤且為最新狀態,即使應用程式處於離線狀態,也能提供最佳體驗。

快取

快取是指應用程式所使用的資料儲存空間。舉例來說,使用者網路連線中斷時,您可能會想要暫時儲存網路的資料。即使已無法使用網路,應用程式仍可借助快取資料。快取也可以為不再顯示於畫面上的活動儲存暫存資料,甚至儲存應用程式啟動期間的持續性資料。

快取可以採用多種形式,有些較為簡單、有些較為複雜,視特定工作而定。以下表格說明在 Android 中實作網路快取的方法。

快取技術

用途

Retrofit 是一個網路程式庫,用於實作 Android 適用的類型安全 REST 用戶端。您可以設定 Retrofit 在本機儲存所有網路結果的副本。

對於簡單的要求和回應、網路呼叫頻率不高或小型資料集來說,這是不錯的解決方式。

您可以使用 DataStore 儲存鍵/值組合。

如果鍵很少且值較為簡單 (例如應用程式設定),這是不錯的解決方式。您無法使用這項技術儲存大量結構化資料。

您可以存取應用程式的內部儲存空間目錄,並將資料檔案儲存在其中。應用程式的套件名稱會指定應用程式的內部儲存空間目錄,這個目錄位於 Android 檔案系統中的特殊位置。目錄僅供您的應用程式使用,而且會在應用程式解除安裝後清除。

如果有檔案系統可以解決的特定需求 (例如您需要儲存媒體檔案或資料檔案,且必須自行管理檔案時),這是不錯的解決方案。您無法使用這項技術儲存應用程式所需查詢的複雜結構化資料。

您可以使用 Room 快取資料。Room 是一個 SQLite 物件對應程式庫,可提供以 SQLite 為基礎的抽象層。

對於複雜的結構化可查詢資料,這是建議的解決方式,因為在裝置的檔案系統中儲存結構化資料的最佳方式就是儲存在本機 SQLite 資料庫。

在這個程式碼研究室中,您將使用 Room,因為這是在裝置的檔案系統中儲存結構化資料的建議方式。DevBytes 應用程式已設定為使用 Room。您的工作是使用存放區模式實作離線快取,將資料層與使用者介面程式碼分開。

5. 實作 VideoRepository

工作:建立存放區

在這項工作中,您會建立存放區來管理在上一個工作中已實作的離線快取。Room 資料庫沒有管理離線快取的邏輯,其中只有可插入、更新、刪除及擷取資料的方法。存放區將使用邏輯擷取網路結果,並讓資料庫保持在最新狀態。

步驟 1:新增存放區

  1. repository/VideosRepository.kt 建立 VideosRepository 類別。傳入 VideosDatabase 物件做為類別的建構函式參數,以存取 DAO 方法。
class VideosRepository(private val database: VideosDatabase) {
}
  1. VideosRepository 類別中,新增名為 refreshVideos()suspend 方法,這個方法沒有引數,且不會傳回任何內容。這個方法是用於重新整理離線快取的 API。
suspend fun refreshVideos() {
}
  1. refreshVideos() 方法中,將協同程式結構定義切換為 Dispatchers.IO,以執行網路和資料庫作業。
suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
   }
}
  1. withContext 區塊內,使用 Retrofit 服務例項 DevByteNetwork 從網路擷取 DevByte 影片播放清單。
val playlist = DevByteNetwork.devbytes.getPlaylist()
  1. refreshVideos() 方法中,從網路擷取播放清單後,將播放清單儲存在 Room 資料庫中。如要儲存播放清單,請使用 VideosDatabase 類別。呼叫 insertAll() DAO 方法,傳入從網路擷取的播放清單。使用 asDatabaseModel() 擴充功能函式,將播放清單對應到資料庫物件。
database.videoDao.insertAll(playlist.asDatabaseModel())
  1. 以下是完整的 refreshVideos() 方法,其中包含追蹤何時該方法會被呼叫的記錄陳述式:
suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
       val playlist = DevByteNetwork.devbytes.getPlaylist()
       database.videoDao.insertAll(playlist.asDatabaseModel())
   }
}

步驟 2:從資料庫擷取資料

在這個步驟中,您會建立 LiveData 物件,以從資料庫中讀取影片播放清單。在資料庫更新時,這個 LiveData 物件會自動更新。附加的片段或活動會使用新的值重新整理。

  1. VideosRepository 類別中,宣告名為 videosLiveData 物件以存放 DevByteVideo 物件清單。使用 database.videoDao 初始化 videos 物件。呼叫 getVideos() DAO 方法。由於 getVideos() 方法會傳回資料庫物件清單,而不是 DevByteVideo 物件清單,因此 Android Studio 會擲回「類型不符」的錯誤。
val videos: LiveData<List<DevByteVideo>> = database.videoDao.getVideos()
  1. 如要修正錯誤,請使用 Transformations.map 將資料庫物件清單轉換成使用 asDomainModel() 轉換函式的網域物件清單。
val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) {
   it.asDomainModel()
}

現在您已為應用程式實作存放區。在下一個工作中,您將使用簡單的重新整理策略,以確保本機資料庫保持在最新狀態。

6. 在 DevByteViewModel 中使用 VideoRepository

工作:使用重新整理策略整合存放區

在這項工作中,您會使用簡單的重新整理策略將存放區與 ViewModel 整合。還會顯示 Room 資料庫的影片播放清單,而不是直接從網路擷取。

資料庫重新整理是更新或重新整理本機資料庫的過程,讓資料庫與網路中的資料保持同步。在這個範例應用程式中,您將使用簡單的重新整理策略,其中向存放區要求資料的模組會負責重新整理本機資料。

在實際應用程式中,這類策略可能會更複雜。例如,程式碼可能會自動在背景重新整理資料 (將頻寬納入考量),或快取使用者接下來最有可能使用的資料。

  1. 在 viewmodels/DevByteViewModel.ktDevByteViewModel 類別內,建立名為 videosRepository 且類型為 VideosRepository 的私人成員變數。透過傳遞單例模式 VideosDatabase 物件將變數執行個體化。
private val videosRepository = VideosRepository(getDatabase(application))
  1. DevByteViewModel 類別中,將 refreshDataFromNetwork() 方法替換為 refreshDataFromRepository() 方法。舊方法 refreshDataFromNetwork() 使用 Retrofit 程式庫從網路擷取影片播放清單。新方法則會從存放區載入影片播放清單。存放區會決定要從哪個來源 (例如網路、資料庫等) 擷取播放清單,而不需在檢視模型中包含實作的詳細資料。存放區也可以使程式碼更容易維護;即使日後要變更取得資料的實作方式,也不需要修改檢視模式。
private fun refreshDataFromRepository() {
   viewModelScope.launch {
       try {
           videosRepository.refreshVideos()
           _eventNetworkError.value = false
           _isNetworkErrorShown.value = false

       } catch (networkError: IOException) {
           // Show a Toast error message and hide the progress bar.
           if(playlist.value.isNullOrEmpty())
               _eventNetworkError.value = true
       }
   }
}
  1. DevByteViewModel 類別的 init 區塊中,將函式呼叫從 refreshDataFromNetwork() 變更為 refreshDataFromRepository()。這個程式碼可從存放區擷取影片播放清單,而不是直接從網路擷取。
init {
   refreshDataFromRepository()
}
  1. DevByteViewModel 類別中,刪除 _playlist 屬性及其幕後屬性 playlist

要刪除的程式碼

private val _playlist = MutableLiveData<List<Video>>()
...
val playlist: LiveData<List<Video>>
   get() = _playlist
  1. DevByteViewModel 類別中,將 videosRepository 物件執行個體化之後,新增一個名為 playlist 的新 val,來保存存放區中的 LiveData 影片清單。
val playlist = videosRepository.videos
  1. 執行應用程式。應用程式會照常運作,但系統現在會從網路擷取 DevBytes 播放清單,並儲存至 Room 資料庫。螢幕上顯示的播放清單是取自 Room 資料庫,而非直接取自網路。

30ee74d946a2f6ca.png

  1. 只要在模擬器或裝置上啟用飛航模式,就可以看出這項差異。
  2. 再次執行應用程式。請注意,應用程式不會顯示「網路錯誤」的浮動式訊息,而會顯示從離線快取中擷取的播放清單。
  3. 在模擬器或裝置上關閉飛航模式。
  4. 關閉再重新開啟應用程式。網路要求在背景執行時,應用程式會從離線快取載入播放清單。

如果有來自網路的新資料,螢幕會自動更新以顯示新資料。不過,DevBytes 伺服器不會重新整理其內容,因此您不會看到資料正在更新。

真厲害!在這個程式碼研究室中,您將離線快取與 ViewModel 整合,以顯示來自存放區的播放清單,而不是從網路擷取的播放清單。

7. 解決方案程式碼

解決方案程式碼

Android Studio 專案:RepositoryPattern

8. 恭喜

恭喜!在本課程中您學到:

  • 快取是將從網路擷取的資料儲存到裝置儲存空間的過程。快取可讓應用程式在裝置離線,或應用程式必須重新存取相同資料時,得以存取資料。
  • 如要讓應用程式在裝置的檔案系統中儲存結構化資料,最好的做法是使用本機 SQLite 資料庫。Room 是 SQLite 物件對應程式庫,意味著其提供了以 SQLite 為基礎的抽象層。使用 Room 是實作離線快取的建議最佳做法。
  • 存放區類別可以將資料來源 (例如 Room 資料庫和網路服務) 與應用程式的其他部分隔離。存放區類別提供簡潔的 API,方便存取應用程式其餘部分的資料。
  • 對於程式碼分隔和架構,使用存放區是建議的最佳做法。
  • 設計離線快取時,最佳做法是將應用程式的網路、網域和資料庫物件做出區隔。這項策略是區隔疑慮的例子之一。

瞭解詳情