リポジトリ パターン

1. 始める前に

はじめに

この Codelab では、オフライン キャッシュを使用してアプリのユーザー エクスペリエンスを改善します。多くのアプリはネットワークのデータを利用します。アプリが起動するたびにサーバーからデータを取得して読み込み画面を表示すると、ユーザー エクスペリエンスが低下し、アプリのアンインストールにつながるおそれがあります。

ユーザーはアプリを起動したとき、データがすぐに表示されることを期待します。これを実現するには、オフライン キャッシュを実装します。オフライン キャッシュとは、アプリがネットワークから取得したデータをデバイスのローカル ストレージに保存することにより、高速にアクセスできるようにすることです。

アプリはネットワークからデータを取得するだけでなく、以前にダウンロードした結果のオフライン キャッシュを保持することもできるため、こうした複数のデータソースをアプリで整理する方法が必要になります。そのためには、アプリのデータについて、信頼できる唯一の情報源として機能するリポジトリ クラスを実装し、ビューモデルからデータソース(ネットワーク、キャッシュなど)を抜き出します。

前提となる知識

以下について把握しておく必要があります。

学習内容

  • アプリのデータレイヤーをアプリの残りの部分から抜き出すためにリポジトリを実装する方法。
  • リポジトリを使用してキャッシュに保存されたデータを読み込む方法。

演習内容

  • リポジトリを使用してデータレイヤーを抜き出し、リポジトリ クラスを ViewModel と統合する。
  • オフライン キャッシュのデータを表示する。

2. スターター コード

プロジェクト コードをダウンロードする

フォルダ名は RepositoryPattern-Starter です。Android Studio でプロジェクトを開くときは、このフォルダを選択してください。

この Codelab のコードを取得して Android Studio で開くには、以下の手順に沿って操作します。

コードを取得する

  1. 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
  2. ブランチ名と Codelab で指定したブランチ名が一致していることを確認します。たとえば、次のスクリーンショットでは、ブランチ名は 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. ファイル ブラウザで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
  2. そのプロジェクト フォルダをダブルクリックします。
  3. Android Studio でプロジェクトが開かれるまで待ちます。
  4. 実行ボタン 8de56cba7583251f.png をクリックして、アプリをビルドし、実行します。期待どおりにビルドされることを確認します。

3. スターター アプリの概要

DevBytes アプリは、Android デベロッパー YouTube チャンネルの DevBytes 動画のリストをリサイクラー ビューに表示します。ユーザーはこれをクリックすると、動画へのリンクを開くことができます。

9757e53b89d2de7c.png

スターター コードは十分に機能しますが、ユーザー エクスペリエンスに悪影響を及ぼす重大な不具合があります。インターネット接続が不安定な場合や、インターネットに接続されていない場合、動画がまったく表示されません。この問題は、以前にアプリを開いたことがある場合でも発生します。ユーザーがアプリを終了し、今度はインターネットに接続せずアプリを再起動すると、アプリは動画のリストの再ダウンロードを試行しますが、失敗します。

これは、エミュレータで実際に確認できます。

  1. Android Emulator で、一時的に機内モードをオンにします(設定アプリ > [ネットワークとインターネット] > [機内モード])。
  2. DevBytes アプリを実行し、画面に何も表示されないことを確認します。

f0365b27d0dd8f78.png

  1. Codelab の残りの部分に進む前に、必ず機内モードをオフにしてください。

これは、DevBytes アプリがデータを初めてダウンロードし、後で使用するためのデータがキャッシュに保存されていないために発生します。このアプリには現在、Room データベースが含まれています。ここでのタスクは、Room データベースを使用してキャッシュ機能を実装し、リポジトリを使用するようにビューモデルを更新することです。そうすることで、新しいデータがダウンロードされるか、Room データベースから取得されるようになります。リポジトリ クラスは、このロジックをビューモデルから抜き出し、コードが整理され分離された状態を維持します。

スターター プロジェクトは、複数のパッケージで構成されています。

25b5f8d0997df54c.png

コードに慣れていただきたいところではありますが、ここでは repository/VideosRepository.ktviewmodels/DevByteViewModel という 2 つのファイルしか扱いません。まず、キャッシュ用のリポジトリ パターンを実装する VideosRepository クラスを作成します(以降のページで詳しく説明します)。次に、新しい VideosRepository クラスを使用するように DevByteViewModel を更新します。

しかし、コードの説明に入る前に、キャッシュとリポジトリ パターンについてもう少し詳しく見てみましょう。

4. キャッシュとリポジトリ パターン

リポジトリ

リポジトリ パターンは、データレイヤーをアプリの残りの部分から分離する設計パターンです。データレイヤーとは、アプリのデータとビジネス ロジックを処理する UI とは別のアプリ部分を指し、アプリの残りの部分がデータにアクセスするための一貫した API を公開します。UI はユーザーに情報を提供しますが、データレイヤーには、ネットワーク コード、Room データベース、エラー処理、データの読み取りや操作を行うコードなどが含まれています。

9e528301efd49aea.png

リポジトリは、データソース(永続モデル、ウェブサービス、キャッシュなど)の間の競合を解決し、このデータに対する変更を一元化できます。次の図は、アクティビティなどのアプリ コンポーネントが、リポジトリによってデータソースとどのようにやり取りするかを示しています。

69021c8142d29198.png

リポジトリを実装するには、別のクラス(次のタスクで作成する VideosRepository クラスなど)を使用します。リポジトリ クラスは、データソースをアプリの残りの部分から分離し、アプリの残りの部分に、データアクセスのためのクリーンな API を提供します。リポジトリ クラスを使用すると、このコードは ViewModel クラスから分離されます。これは、コードの分離とアーキテクチャに関して推奨されるベスト プラクティスです。

リポジトリを使用するメリット

リポジトリ モジュールはデータ オペレーションを処理し、複数のバックエンドを使用できるようにします。一般的な実際のアプリでは、データをネットワークから取得するか、ローカル データベースにキャッシュ保存された結果を使用するかを決定するロジックを、リポジトリで実装します。リポジトリを使用すると、ビューモデルなどの呼び出し元コードに影響を与えることなく、別の永続ライブラリに移行するなど、実装の詳細を入れ替えることができます。また、コードをモジュール化し、テストしやすくするためにも役立ちます。リポジトリを簡単にモックアップして、残りのコードをテストできます。

リポジトリは、アプリのデータの特定部分に対する、信頼できる唯一の情報源として機能する必要があります。ネットワーク上のリソースやオフライン キャッシュなど、複数のデータソースを扱う場合、リポジトリはアプリのデータを可能な限り正確かつ最新に保ち、アプリがオフラインのときでも最高のエクスペリエンスを提供します。

キャッシュ

キャッシュとは、アプリが使用するデータを保存することです。たとえば、ユーザーのインターネット接続が中断した場合に備えて、ネットワークのデータを一時的に保存できます。ネットワークが利用できなくなった場合でも、アプリはキャッシュに保存されたデータを利用できます。キャッシュは、画面に表示されなくなったアクティビティの一時データを保存する場合や、アプリの起動間でデータを保持する場合にも便利です。

キャッシュは具体的なタスクに応じて、単純なものから複雑なものまで、さまざまな形をとることができます。Android でネットワーク キャッシュを実装する方法を次の表に示します。

キャッシュ手法

用途

Retrofit は、Android 用のタイプセーフな REST クライアントを実装するために使用されるネットワーキング ライブラリです。すべてのネットワーク結果のコピーをローカルに保存するように Retrofit を設定できます。

単純なリクエストとレスポンス、頻度の低いネットワーク呼び出し、小規模なデータセットに適したソリューションです。

DataStore を使用して Key-Value ペアを保存できます。

アプリの設定など、少数のキーや単純な値に適したソリューションです。この手法は、大量の構造化データの保存には使用できません。

アプリの内部ストレージ ディレクトリにアクセスして、そこにデータファイルを保存できます。アプリのパッケージ名で、Android ファイル システム内の特別な場所にある、アプリの内部ストレージ ディレクトリが指定されています。このディレクトリはアプリ専用のディレクトリであり、アプリをアンインストールすると消去されます。

たとえば、メディア ファイルやデータファイルを保存する必要があり、自分でファイルを管理しなければならない場合など、ファイル システムで解決できる特定のニーズがある場合に適したソリューションです。この手法は、アプリでクエリする必要がある複雑な構造化データの保存には使用できません。

データをキャッシュに保存するには Room を使用します。Room は SQLite に抽象化レイヤを提供する SQLite オブジェクト マッピング ライブラリです。

デバイスのファイル システムに構造化データを保存する最善の方法はローカルの SQLite データベースであるため、複雑なクエリ可能構造化データに推奨されるソリューションです。

この Codelab では、デバイスのファイル システムに構造化データを保存するおすすめの方法として、Room を使用します。DevBytes アプリはすでに Room を使用するように設定されています。ここでのタスクは、リポジトリ パターンを使用してオフライン キャッシュを実装し、データレイヤを UI コードから分離することです。

5. VideosRepository を実装する

タスク: リポジトリを作成する

このタスクでは、前のタスクで実装した、オフライン キャッシュを管理するためのリポジトリを作成します。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 クラスで、videos という LiveData オブジェクトを宣言し、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 で VideosRepository を使用する

タスク: 更新戦略を使用してリポジトリを統合する

このタスクでは、シンプルな更新戦略を使用してリポジトリを 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 オブジェクトをインスタンス化した後、リポジトリからの動画の LiveData リストを保持するために、playlist という新しい val を追加します。
val playlist = videosRepository.videos
  1. アプリを実行します。アプリは以前と同じように動作しますが、今度は DevBytes プレイリストがネットワークから取得され、Room データベースに保存されます。プレイリストはネットワークから直接ではなく、Room データベースから画面に表示されます。

30ee74d946a2f6ca.png

  1. 違いを確認するには、エミュレータまたはデバイスの機内モードを有効にします。
  2. アプリをもう一度実行します。「ネットワーク エラー」のトースト メッセージは表示されません。プレイリストはオフライン キャッシュから取得され、表示されます。
  3. エミュレータまたはデバイスの機内モードをオフにします。
  4. アプリを閉じて、もう一度開きます。アプリはオフライン キャッシュからプレイリストを読み込みますが、ネットワーク リクエストはバックグラウンドで行われます。

ネットワークから新しいデータが届くと、画面が自動的に更新されて新しいデータが表示されます。ただし、DevBytes サーバーはコンテンツを更新しないため、データの更新は確認できません。

おつかれさまでした。この Codelab では、プレイリストをネットワークから取得するのではなく、リポジトリからプレイリストを表示するために、オフライン キャッシュを ViewModel と統合しました。

7. 解答コード

解答コード

Android Studio プロジェクト: RepositoryPattern

8. 完了

今回は、以下の内容を学習しました。

  • キャッシュは、ネットワークから取得したデータをデバイスのストレージに保存するプロセスです。キャッシュを使用すると、デバイスがオフラインのときや、アプリが同じデータに再度アクセスする必要がある場合に、アプリがデータにアクセスできます。
  • アプリで構造化データをデバイスのファイル システムに格納する最善の方法は、ローカルの SQLite データベースを使用することです。Room は SQLite オブジェクト マッピング ライブラリであり、SQLite に抽象化レイヤを提供します。オフライン キャッシュの実装で推奨されるベスト プラクティスは、Room を使用することです。
  • リポジトリ クラスは、Room データベースやウェブサービスなどのデータソースを、アプリの残りの部分から分離します。リポジトリ クラスは、アプリの残りの部分に、データアクセスのためのクリーンな API を提供します。
  • リポジトリを使用することは、コードの分離とアーキテクチャに関して推奨されるベスト プラクティスです。
  • オフライン キャッシュを設計する際のベスト プラクティスは、アプリのネットワーク、ドメイン、データベースのオブジェクトを分離することです。この戦略は、関心の分離の一例です。

詳細