リポジトリと手動 DI を追加する

1. 始める前に

はじめに

前の Codelab では、ViewModel を通じて API サービスを使用してネットワークから火星写真の URL を取得することで、ウェブサービスからデータを取得する方法を学習しました。この方法は効果的で簡単に実装できますが、アプリの成長に合わせてうまく拡張することができず、複数のデータソースと連携する必要があります。この問題に対処するため、Android アーキテクチャのベスト プラクティスでは、UI レイヤとデータレイヤを分離することをおすすめします。

この Codelab では、Mars Photos アプリを UI レイヤとデータレイヤに別々にリファクタリングします。リポジトリ パターンを実装し、依存関係インジェクションを使用する方法を学びます。依存関係インジェクションを使用して、開発とテストに役立つ、より柔軟なコーディング構造を作成します。

前提条件

  • REST ウェブサービスから JSON を取得する能力と、Retrofit ライブラリおよび シリアル化(kotlinx.serialization)ライブラリを使用してそのデータを解析し、Kotlin オブジェクトに変換する能力
  • REST ウェブサービスの使用方法に関する知識
  • アプリにコルーチンを実装する能力

学習内容

  • リポジトリ パターン
  • 依存関係インジェクション

作成するアプリの概要

  • UI レイヤとデータレイヤに分離されるよう Mars Photos アプリを変更する。
  • データレイヤを分離しつつ、リポジトリ パターンを実装する。
  • 依存関係インジェクションを使用して、疎結合のコードベースを作成する。

必要なもの

  • 最新のウェブブラウザ(Chrome の最新バージョンなど)を搭載したパソコン

スターター コードを取得する

まず、スターター コードをダウンロードします。

または、GitHub リポジトリのクローンを作成してコードを入手することもできます。

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout repo-starter

コードは Mars Photos GitHub リポジトリで確認できます。

2. UI レイヤとデータレイヤを分離する

レイヤを分離する理由

コードを異なるレイヤに分割することで、アプリのスケーラビリティと堅牢性を高め、テストを容易にできます。境界が明確に定義された複数のレイヤを使用することで、複数のデベロッパーは、互いに悪影響を及ぼすことなく同じアプリ上で作業することが容易になります。

Android の推奨アプリ アーキテクチャによれば、アプリには少なくとも UI レイヤとデータレイヤがあるべきです。

この Codelab では、データレイヤーに焦点を当て、推奨されるベスト プラクティスに従うようアプリに変更を加えます。

データレイヤーとは

データレイヤーは、アプリのビジネス ロジックの処理と、アプリのデータの収集と保存を担当します。データレイヤは、単方向データフロー パターンを使用して、UI レイヤにデータを公開します。データは、ネットワーク リクエスト、ローカル データベースなどの複数のソースや、デバイス上のファイルから取得される場合があります。

アプリに複数のデータソースがある場合もあります。アプリを開くと、デバイス上のローカル データベース(最初のソース)からデータが取得されます。アプリの実行中に、新しいデータの取得のために 2 番目のソースにネットワーク リクエストが発行されます。

データを UI コードとは別のレイヤに置くことで、コードの一部を変更でき、他の部分に影響を与えることがありません。この方法は、関心の分離と呼ばれる設計原則の一部です。コードの一部は、それ自体の関心に焦点を当て、内部動作を他のコードから分離してカプセル化します。カプセル化とは、コードの内部動作をコードの他の部分から隠す形式です。コードの 1 つのセクションが別のコードのセクションとやり取りする必要がある場合、カプセル化はインターフェースを介して行われます。

UI レイヤの関心は、提供されたデータを表示することです。データはデータレイヤの関心であるため、UI はデータを取得しなくなります。

データレイヤは、1 つ以上のリポジトリで構成されています。リポジトリ自体には、0 個以上のデータソースが含まれています。

dbf927072d3070f0.png

ベスト プラクティスとして、アプリで使用するデータソースのタイプごとにリポジトリを提供する必要があります。

この Codelab では、アプリに 1 つのデータソースがあるため、コードをリファクタリングした後でアプリに 1 つのリポジトリが作成されます。このアプリの場合、インターネットからデータを取得するリポジトリが、データソースの役割を果たします。これは、API に対するネットワーク リクエストによって行われます。データソースのコーディングがより複雑になるか、新しいデータソースが追加される場合、データソースの責任はさまざまなデータソース クラスにカプセル化され、リポジトリはすべてのデータソースの管理を担当します。

リポジトリとは

リポジトリ クラスには通常、次のような役割があります。

  • アプリの他の部分にデータを公開する。
  • データの変更を一元管理する。
  • 複数のデータソース間の競合を解決する。
  • アプリの他の部分からデータソースを抽象化する。
  • ビジネス ロジックを含む。

Mars Photos アプリには、ネットワーク API 呼び出しという 1 つのデータソースがあります。このアプリはデータを取得するだけなので、ビジネス ロジックを含みません。データはリポジトリ クラスを介してアプリに公開され、リポジトリ クラスはデータのソースを抽象化します。

ff7a7cd039402747.png

3. データレイヤを作成する

まず、リポジトリ クラスを作成する必要があります。Android デベロッパー ガイドには、リポジトリ クラスの名前は、担当するデータに基づいて付けられると記載されています。リポジトリの命名規則は、「データのタイプ + リポジトリ」です。このアプリの場合は MarsPhotosRepository になります。

リポジトリを作成する

  1. [com.example.marsphotos] を右クリックして [New] > [Package] を選択します。
  2. ダイアログで「data」と入力します。
  3. data パッケージを右クリックして、[New] > [Kotlin Class/File] を選択します。
  4. ダイアログで [Interface] を選択し、インターフェースの名前として「MarsPhotosRepository」と入力します。
  5. MarsPhotosRepository インターフェース内に getMarsPhotos() という抽象関数を追加します。この関数は MarsPhoto オブジェクトのリストを返します。呼び出しはコルーチンから行われるため、宣言には suspend を使用します。
import com.example.marsphotos.model.MarsPhoto

interface MarsPhotosRepository {
    suspend fun getMarsPhotos(): List<MarsPhoto>
}
  1. インターフェース宣言の下に、NetworkMarsPhotosRepository という名前のクラスを作成して MarsPhotosRepository インターフェースを実装します。
  2. インターフェース MarsPhotosRepository をクラス宣言に追加します。

インターフェースの抽象メソッドをオーバーライドしていなかったため、エラー メッセージが表示されます。次の手順でこのエラーを解消します。

Android Studio のスクリーンショット。MarsPhotosRepository インターフェースとネットワーク NetworkMarsPhotosRepository

  1. NetworkMarsPhotosRepository クラス内で、抽象関数 getMarsPhotos() をオーバーライドします。この関数は、MarsApi.retrofitService.getPhotos() の呼び出しでデータを返します。
import com.example.marsphotos.network.MarsApi

class NetworkMarsPhotosRepository() : MarsPhotosRepository {
   override suspend fun getMarsPhotos(): List<MarsPhoto> {
       return MarsApi.retrofitService.getPhotos()
   }
}

次に、Android のベスト プラクティスで推奨されているように、リポジトリを使用してデータを取得するように ViewModel コードを更新する必要があります。

  1. ui/screens/MarsViewModel.kt ファイルを開きます。
  2. 下にスクロールして getMarsPhotos() メソッドを表示します。
  3. val listResult = MarsApi.retrofitService.getPhotos()」という行を次のコードで置き換えます。
import com.example.marsphotos.data.NetworkMarsPhotosRepository

val marsPhotosRepository = NetworkMarsPhotosRepository()
val listResult = marsPhotosRepository.getMarsPhotos()

5313985852c151aa.png

  1. アプリを実行します。お気づきのように、表示される結果は前の結果と同じです。

ViewModel がデータのネットワーク リクエストを直接行うのではなく、リポジトリがデータを提供します。ViewModelMarsApi コードを直接参照しなくなりました。フロー図。以前は Viewmodel からデータレイヤーに直接アクセス。現在は MarsPhotosRepository を追加

この方法により、データを取得するコードが ViewModel から疎結合されます。疎結合されると、リポジトリに getMarsPhotos() という関数がある限り、ViewModel またはリポジトリに変更を加えても、他の部分に悪影響を与えることはありあせん。

呼び出し元に影響を与えることなく、リポジトリ内の実装に変更を加えることができるようになりました。大規模なアプリでは、この変更により複数の呼び出し元をサポートできます。

4. 依存関係インジェクション

多くの場合、クラスが機能するには、他のクラスのオブジェクトが必要になります。クラスで別のクラスが必要な場合、必要なクラスは依存関係と呼ばれます。

次の例では、Car オブジェクトが Engine オブジェクトに依存しています。

クラスでこれらの必要なオブジェクトを取得する方法は 2 つあります。1 つ目の方法は、クラスが必要なオブジェクトをインスタンス化することです。

interface Engine {
    fun start()
}

class GasEngine : Engine {
    override fun start() {
        println("GasEngine started!")
    }
}

class Car {

    private val engine = GasEngine()

    fun start() {
        engine.start()
    }
}

fun main() {
    val car = Car()
    car.start()
}

もう 1 つの方法は、必要なオブジェクトを引数として渡すことです。

interface Engine {
    fun start()
}

class GasEngine : Engine {
    override fun start() {
        println("GasEngine started!")
    }
}

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main() {
    val engine = GasEngine()
    val car = Car(engine)
    car.start()
}

クラスが必要なオブジェクトをインスタンス化することは簡単ですが、この方法では、クラスと必要なオブジェクトが密結合されているため、コードの柔軟性がなくなり、テストが難しくなります。

呼び出し元のクラスは、オブジェクトのコンストラクタ(実装の詳細)を呼び出す必要があります。コンストラクタを変更する場合は、呼び出し元のコードも変更する必要があります。

コードの柔軟性と適応性を高めるために、クラスは依存するオブジェクトをインスタンス化することはできません。依存するオブジェクトは、クラスの外部でインスタンス化してから渡す必要があります。この方法では、クラスが特定の 1 つのオブジェクトにハードコードされなくなるため、より柔軟なコードを作成できます。必要なオブジェクトの実装は、呼び出し元コードを変更しなくても変更できます。

前の例を続けると、ElectricEngine が必要な場合は、これを作成して Car クラスに渡すことができます。Car クラスは一切変更する必要がありません。

interface Engine {
    fun start()
}

class ElectricEngine : Engine {
    override fun start() {
        println("ElectricEngine started!")
    }
}

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main() {
    val engine = ElectricEngine()
    val car = Car(engine)
    car.start()
}

必要なオブジェクトを渡すことを「依存関係インジェクション」(DI)といいます。「制御の反転」ともいいます。

DI とは、呼び出し元クラスにハードコードされるのではなく、実行時に依存関係を提供することを指します。

依存関係インジェクションを実装すると次のことが可能になります。

  • コードの再利用に役立つ。コードは特定のオブジェクトに依存しないため、柔軟性が高くなります。
  • リファクタリングが容易になる。コードは疎結合されているため、コードの 1 つのセクションをリファクタリングしても、コードの別のセクションに影響はありません。
  • テストに役立つ。テスト オブジェクトはテスト中に渡すことができます。

DI がテストに役立つ方法の一例として、ネットワーク呼び出しコードをテストするというものがあります。このテストでは、ネットワーク呼び出しが行われ、データが返されるかどうかを実際にテストしてみます。テスト中にネットワーク リクエストを行うたびに支払いが発生する場合は、コストが高くなる可能性があるため、このコードのテストをスキップできます。では、テスト用にネットワーク リクエストを模擬できる場合はどうでしょうか。どのくらいコストを削減できるでしょうか。テスト用に、ネットワーク呼び出しを実際に行わなくても、リポジトリにテスト オブジェクトを渡して、呼び出し時の架空のデータを返すことができます。1ea410d6670b7670.png

ViewModel をテスト可能にしたいのですが、現在は実際のネットワーク呼び出しを行うリポジトリに依存しています。実際の本番環境リポジトリでテストすると、多くのネットワーク呼び出しが行われます。この問題を解決するには、ViewModel がリポジトリを作成するのではなく、本番環境とテストに使用するリポジトリ インスタンスを動的に決定して渡す方法が必要です。

このプロセスは、リポジトリを MarsViewModel に提供するアプリケーション コンテナを実装することで行われます。

コンテナは、アプリに必要な依存関係を含むオブジェクトです。これらの依存関係はアプリ全体で使用されるため、すべてのアクティビティが使用できる共通の場所に配置する必要があります。Application クラスのサブクラスを作成して、コンテナへの参照を格納できます。

アプリケーション コンテナを作成する

  1. data パッケージを右クリックして、[New] > [Kotlin Class/File] を選択します。
  2. ダイアログで [Interface] を選択し、インターフェースの名前として「AppContainer」と入力します。
  3. AppContainer インターフェース内に、MarsPhotosRepository 型の marsPhotosRepository という抽象プロパティを追加します。7ed26c6dcf607a55.png
  4. インターフェース定義の下に、インターフェース AppContainer を実装する DefaultAppContainer というクラスを作成します。
  5. network/MarsApiService.kt から、変数 BASE_URLretrofitretrofitService のコードを DefaultAppContainer クラスに移動して、依存関係を維持しているコンテナ内にすべて配置されるようにします。
import retrofit2.Retrofit
import com.example.marsphotos.network.MarsApiService
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType

class DefaultAppContainer : AppContainer {

    private const val BASE_URL =
        "https://android-kotlin-fun-mars-server.appspot.com"

    private val retrofit: Retrofit = Retrofit.Builder()
        .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
        .baseUrl(BASE_URL)
        .build()

    private val retrofitService: MarsApiService by lazy {
        retrofit.create(MarsApiService::class.java)
    }

}
  1. 変数 BASE_URL の場合、const キーワードを削除します。BASE_URL はトップレベル変数ではなくなり、DefaultAppContainer クラスのプロパティになったため、const を削除する必要があります。キャメルケース baseUrl にリファクタリングします。
  2. 変数 retrofitService の場合は、private 可視性修飾子を追加します。private 修飾子を追加したのは、変数 retrofitService がプロパティ marsPhotosRepository によってクラス内でのみ使用され、クラス外からアクセスできるようにする必要がないためです。
  3. DefaultAppContainer クラスはインターフェース AppContainer を実装しているため、marsPhotosRepository プロパティをオーバーライドする必要があります。変数 retrofitService の後に、次のコードを追加します。
override val marsPhotosRepository: MarsPhotosRepository by lazy {
    NetworkMarsPhotosRepository(retrofitService)
}

完成した DefaultAppContainer クラスは次のようになります。

class DefaultAppContainer : AppContainer {

    private val baseUrl =
        "https://android-kotlin-fun-mars-server.appspot.com"

    /**
     * Use the Retrofit builder to build a retrofit object using a kotlinx.serialization converter
     */
    private val retrofit = Retrofit.Builder()
        .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
        .baseUrl(baseUrl)
        .build()
    
    private val retrofitService: MarsApiService by lazy {
        retrofit.create(MarsApiService::class.java)
    }

    override val marsPhotosRepository: MarsPhotosRepository by lazy {
        NetworkMarsPhotosRepository(retrofitService)
    }
}
  1. data/MarsPhotosRepository.kt ファイルを開きます。retrofitServiceNetworkMarsPhotosRepository に渡しているため、NetworkMarsPhotosRepository クラスを変更する必要があります。
  2. 次のコードに示すように、NetworkMarsPhotosRepository クラス宣言にコンストラクタ パラメータ marsApiService を追加します。
import com.example.marsphotos.network.MarsApiService

class NetworkMarsPhotosRepository(
    private val marsApiService: MarsApiService
) : MarsPhotosRepository {
  1. NetworkMarsPhotosRepository クラスの getMarsPhotos() 関数内の return 文を変更して marsApiService からデータを取得します。
override suspend fun getMarsPhotos(): List<MarsPhoto> = marsApiService.getPhotos()
}
  1. MarsPhotosRepository.kt ファイルから次のインポートを削除します。
// Remove
import com.example.marsphotos.network.MarsApi

network/MarsApiService.kt ファイルのすべてのコードをオブジェクトから移動しました。残りのオブジェクト宣言は不要になったため、削除できます。

  1. 次のコードを削除します。
object MarsApi {

}

5. アプリケーション コンテナをアプリにアタッチする

このセクションの手順では、次の図に示すように、アプリのオブジェクトをアプリケーション コンテナに接続します。

92e7d7b79c4134f0.png

  1. com.example.marsphotos を右クリックして、[New] > [Kotlin Class/File] を選択します。
  2. ダイアログで「MarsPhotosApplication」と入力します。このクラスはアプリのオブジェクトから継承されるため、クラス宣言に追加する必要があります。
import android.app.Application

class MarsPhotosApplication : Application() {
}
  1. MarsPhotosApplication クラス内で、AppContainer 型の container という変数を宣言して DefaultAppContainer オブジェクトを保存します。この変数は onCreate() の呼び出し中に初期化されるため、lateinit 修飾子でマークする必要があります。
import com.example.marsphotos.data.AppContainer
import com.example.marsphotos.data.DefaultAppContainer

lateinit var container: AppContainer
override fun onCreate() {
    super.onCreate()
    container = DefaultAppContainer()
}
  1. 完全な MarsPhotosApplication.kt ファイルは次のコードのようになります。
package com.example.marsphotos

import android.app.Application
import com.example.marsphotos.data.AppContainer
import com.example.marsphotos.data.DefaultAppContainer

class MarsPhotosApplication : Application() {
    lateinit var container: AppContainer
    override fun onCreate() {
        super.onCreate()
        container = DefaultAppContainer()
    }
}
  1. 定義したアプリクラスをアプリで使用できるように、Android マニフェストを更新する必要があります。manifests/AndroidManifest.xml ファイルを開きます。

759144e4e0634ed8.png

  1. application セクションで、android:name 属性を追加し、アプリクラス名の値を ".MarsPhotosApplication" にします。
<application
   android:name=".MarsPhotosApplication"
   android:allowBackup="true"
...
</application>

6. ViewModel にリポジトリを追加する

これらの手順を完了すると、ViewModel はリポジトリ オブジェクトを呼び出して火星のデータを取得できます。

7425864315cb5e6f.png

  1. ui/screens/MarsViewModel.kt ファイルを開きます。
  2. MarsViewModel のクラス宣言に、MarsPhotosRepository 型のプライベート コンストラクタ パラメータ marsPhotosRepository を追加します。アプリが依存関係インジェクションを使用するようになったため、コンストラクタ パラメータの値はアプリケーション コンテナから取得されます。
import com.example.marsphotos.data.MarsPhotosRepository

class MarsViewModel(private val marsPhotosRepository: MarsPhotosRepository) : ViewModel(){
  1. getMarsPhotos() 関数では、marsPhotosRepository がコンストラクタ呼び出しに入力されるようになったため、次のコード行を削除します。
val marsPhotosRepository = NetworkMarsPhotosRepository()
  1. Android フレームワークでは、ViewModel を作成する際にコンストラクタで値を渡すことができません。この制限を回避するために ViewModelProvider.Factory オブジェクトを実装します。

Factory パターンは、オブジェクトの作成に使用される作成パターンです。MarsViewModel.Factory オブジェクトはアプリケーション コンテナを使用して marsPhotosRepository を取得します。次に、ViewModel オブジェクトの作成時にこのリポジトリを ViewModel に渡します。

  1. 関数 getMarsPhotos() の下に、コンパニオン オブジェクトのコードを入力します。

コンパニオン オブジェクトは、すべてのユーザーが使用するオブジェクトのインスタンスを 1 つ使用できる点で便利です。高価なオブジェクトのインスタンスを新たに作成する必要はありません。これは実装の詳細であり、分離することで、アプリのコードの他の部分に影響を与えずに変更を加えることができます。

APPLICATION_KEYViewModelProvider.AndroidViewModelFactory.Companion オブジェクトの一部で、アプリの MarsPhotosApplication オブジェクトの検出に使用されます。このオブジェクトには、依存関係インジェクションに使用されるリポジトリを取得するための container プロパティがあります。

import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.example.marsphotos.MarsPhotosApplication

companion object {
   val Factory: ViewModelProvider.Factory = viewModelFactory {
       initializer {
           val application = (this[APPLICATION_KEY] as MarsPhotosApplication)
           val marsPhotosRepository = application.container.marsPhotosRepository
           MarsViewModel(marsPhotosRepository = marsPhotosRepository)
       }
   }
}
  1. theme/MarsPhotosApp.kt ファイルを開き、MarsPhotosApp() 関数内で、Factory を使用するよう viewModel() を更新します。
Surface(
            // ...
        ) {
            val marsViewModel: MarsViewModel =
   viewModel(factory = MarsViewModel.Factory)
            // ...
        }

この marsViewModel 変数は、viewModel() 関数への呼び出しによって入力されます。この関数は、引数としてコンパニオン オブジェクトの MarsViewModel.Factory を渡されて ViewModel を作成します。

  1. アプリを実行して、以前と同様に動作していることを確認します。

これで、Mars Photos アプリをリファクタリングしてリポジトリと依存関係インジェクションを使用できるようになりました。リポジトリを使用してデータレイヤを実装することにより、Android のベスト プラクティスに沿って UI とデータソースのコードを分離しています。

依存関係インジェクションを使用すると、ViewModel のテストが容易になります。アプリの柔軟性と堅牢性が高まり、拡張の準備が整いました。

これらの改善を行ったら、次にこのテスト方法を学びます。テストを通じてコードの動作が想定どおりに保たれ、引き続きコードを扱う際にバグが発生する可能性が低くなります。

7. ローカルテストのセットアップを行う

前のセクションでは、REST API サービスとの直接のやり取りを ViewModel から抽象化するためのリポジトリを実装しました。この方法では、目的が限られている小規模なコードをテストできます。機能に制限がある小規模なコード向けのテストは、複数の機能を備えた大規模なコード用に記述したテストよりも、ビルド、実装、理解が容易です。

また、インターフェース、継承、依存関係インジェクションを活用して、リポジトリを実装しました。以降のセクションでは、これらのアーキテクチャのベスト プラクティスによってテストが容易になる理由を説明します。さらに、Kotlin コルーチンを使用してネットワーク リクエストを実行しました。コルーチンを使用するコードをテストするには、コードの非同期実行を考慮するために追加の手順が必要です。これらの手順については、この Codelab で後ほど説明します。

ローカルテストの依存関係を追加する

app/build.gradle.kts に次の依存関係を追加します。

testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1")

ローカルテスト ディレクトリを作成する

  1. プロジェクト ビューで src ディレクトリを右クリックし、[New] > [Directory] > [test/java] を選択してローカル テスト ディレクトリを作成します。
  2. テスト ディレクトリに com.example.marsphotos という名前の新しいパッケージを作成します。

8. テスト用の架空のデータと依存関係を作成する

このセクションでは、依存関係インジェクションがローカルテストの作成にどのように役立つかを説明します。Codelab の前半で、API サービスに依存するリポジトリを作成しました。次に、リポジトリに依存するように ViewModel を変更しました。

各ローカルテストで 1 つの項目のみをテストします。たとえば、ビューモデルの機能をテストしても、リポジトリや API サービスの機能をテストしない場合があります。同様に、リポジトリをテストするときに、API サービスをテストしない場合があります。

インターフェースを使用し、次いで依存関係インジェクションを使用して、これらのインターフェースから継承するクラスを含めると、テスト用に作成された架空のクラスを使用して依存関係の機能をシミュレートできます。テスト用の架空のクラスとデータソースを挿入することで、再現性と整合性を保ちながらコードを個別にテストできます。

まず必要なのは、架空のデータを作成して、後で作成する架空のクラスで使用できるようにすることです。

  1. テスト ディレクトリで、com.example.marsphotos の下に fake というパッケージを作成します。
  2. fake ディレクトリに FakeDataSource という新しい Kotlin オブジェクトを作成します。
  3. このオブジェクトで、MarsPhoto オブジェクトのリストに設定するプロパティを作成します。リストは長くする必要はありませんが、少なくとも 2 つのオブジェクトを含める必要があります。
object FakeDataSource {

   const val idOne = "img1"
   const val idTwo = "img2"
   const val imgOne = "url.1"
   const val imgTwo = "url.2"
   val photosList = listOf(
       MarsPhoto(
           id = idOne,
           imgSrc = imgOne
       ),
       MarsPhoto(
           id = idTwo,
           imgSrc = imgTwo
       )
   )
}

この Codelab で前述したように、リポジトリは API サービスに依存しています。リポジトリ テストを作成するには、作成した架空のデータを返す架空の API サービスが必要です。この架空の API サービスがリポジトリに渡されると、リポジトリは架空の API サービスのメソッドが呼び出されたときに架空のデータを受け取ります。

  1. fake パッケージで、FakeMarsApiService という名前の新しいクラスを作成します。
  2. MarsApiService インターフェースを継承するように FakeMarsApiService クラスをセットアップします。
class FakeMarsApiService : MarsApiService {
}
  1. getPhotos() 関数をオーバーライドします。
override suspend fun getPhotos(): List<MarsPhoto> {
}
  1. getPhotos() メソッドから架空の写真のリストを返します。
override suspend fun getPhotos(): List<MarsPhoto> {
   return FakeDataSource.photosList
}

なお、このクラスの目的がよくわからなくても問題ありません。この架空のクラスの使用法については、次のセクションで詳しく説明します。

9. リポジトリ テストを作成する

このセクションでは、NetworkMarsPhotosRepository クラスの getMarsPhotos() メソッドをテストします。さらに、架空のクラスの使用方法を説明し、コルーチンをテストする方法を示します。

  1. 架空のディレクトリに、NetworkMarsRepositoryTest という名前の新しいクラスを作成します。
  2. 作成したクラス内に networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() という新しいメソッドを作成し、@Test アノテーションを付けます。
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList(){
}

リポジトリをテストするには、NetworkMarsPhotosRepository のインスタンスが必要になります。このクラスは MarsApiService インターフェースに依存することを思い出してください。ここで、前のセクションの架空の API サービスを活用します。

  1. NetworkMarsPhotosRepository のインスタンスを作成し、FakeMarsApiServicemarsApiService パラメータとして渡します。
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList(){
    val repository = NetworkMarsPhotosRepository(
       marsApiService = FakeMarsApiService()
    )
}

架空の API サービスを渡すことで、リポジトリ内の marsApiService プロパティの呼び出しにより、FakeMarsApiService が呼び出されます。依存関係に架空のクラスを渡すことで、依存関係が返す内容を正確に制御できます。この方法により、テスト対象のコードは、テストされていないコードや、変更の可能性や予期しない問題が発生する可能性がある API に依存しなくなります。このような状況では、作成したコードに問題がなくても、テストが失敗することがあります。架空の実装は、より一貫性のあるテスト環境の作成、テストの不安定性の低減、1 つの機能をテストする簡潔なテストに役立ちます。

  1. getMarsPhotos() メソッドから返されたデータが FakeDataSource.photosList と等しいことを確認します。
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList(){
    val repository = NetworkMarsPhotosRepository(
       marsApiService = FakeMarsApiService()
    )assertEquals(FakeDataSource.photosList, repository.getMarsPhotos())
}

IDE では、getMarsPhotos() メソッド呼び出しに赤色の下線が表示されます。

2bd5f8999e0f3ec2.png

メソッドにカーソルを合わせると、「suspend 関数「getMarsPhotos」をコルーチンまたは別の suspend 関数からのみ呼び出す必要があります」というツールチップが表示されます。

d2d3b6d770677ef6.png

data/MarsPhotosRepository.kt で、NetworkMarsPhotosRepositorygetMarsPhotos() 実装を見ると、getMarsPhotos() 関数が suspend 関数であることがわかります。

class NetworkMarsPhotosRepository(
   private val marsApiService: MarsApiService
) : MarsPhotosRepository {
   /** Fetches list of MarsPhoto from marsApi*/
   override suspend fun getMarsPhotos(): List<MarsPhoto> = marsApiService.getPhotos()
}

この関数を MarsViewModel から呼び出す際には、このメソッドを viewModelScope.launch() に渡されたラムダの呼び出しを通じてコルーチンから呼び出しています。テストのコルーチンから getMarsPhotos() などの suspend 関数を呼び出す必要もあります。ただし、方法は異なります。次のセクションでは、この問題を解決する方法を説明します。

コルーチンをテストする

このセクションでは、テストメソッドの本文がコルーチンから実行されるように networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() テストを変更します。

  1. NetworkMarsRepositoryTest.kt で、networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() 関数を式に変更します。
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
  1. この式を runTest() 関数と同じになるように設定します。このメソッドではラムダが想定されています。
...
import kotlinx.coroutines.test.runTest
...

@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
    runTest {}

コルーチン テスト ライブラリには runTest() 関数が用意されています。この関数は、ラムダで渡されたメソッドを受け取り、TestScopeCoroutineScope から継承)から実行します。

  1. テスト関数の内容をラムダ関数に移動します。
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
   runTest {
       val repository = NetworkMarsPhotosRepository(
           marsApiService = FakeMarsApiService()
       )
       assertEquals(FakeDataSource.photosList, repository.getMarsPhotos())
   }

getMarsPhotos() の赤色の線が消えています。これを実行すると、テストに合格します。

10. ViewModel テストを作成する

このセクションでは、MarsViewModel から getMarsPhotos() 関数のテストを作成します。MarsViewModelMarsPhotosRepository に依存します。したがって、このテストを作成するには、架空の MarsPhotosRepository を作成する必要があります。また、runTest() メソッドを使用するだけでなく、コルーチンにとって考慮すべき追加の手順がいくつかあります。

架空のリポジトリを作成する

このステップの目的は、MarsPhotosRepository インターフェースから継承した架空のクラスを作成し、getMarsPhotos() 関数をオーバーライドして架空のデータを返すことです。この方法は、架空の API サービスで採用する方法と似ていますが、このクラスで MarsApiService ではなく MarsPhotosRepository インターフェースを拡張する点が異なります。

  1. fake ディレクトリに FakeNetworkMarsPhotosRepository という新しいクラスを作成します。
  2. MarsPhotosRepository インターフェースを使用してこのクラスを拡張します。
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
}
  1. getMarsPhotos() 関数をオーバーライドします。
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
   override suspend fun getMarsPhotos(): List<MarsPhoto> {
   }
}
  1. getMarsPhotos() 関数から FakeDataSource.photosList を返します。
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
   override suspend fun getMarsPhotos(): List<MarsPhoto> {
       return FakeDataSource.photosList
   }
}

ViewModel テストを作成する

  1. MarsViewModelTest という新しいクラスを作成します。
  2. marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() という関数を作成し、@Test アノテーションを付けます。
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()
  1. この関数を runTest() メソッドの結果に設定される式にして、前のセクションのリポジトリ テストと同様に、コルーチンからテストを実行します。
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
    runTest{
    }
  1. runTest() のラムダ本体で、MarsViewModel のインスタンスを作成し、作成した架空のリポジトリのインスタンスを渡します。
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
    runTest{
        val marsViewModel = MarsViewModel(
            marsPhotosRepository = FakeNetworkMarsPhotosRepository()
         )
    }
  1. ViewModel インスタンスの marsUiState について、MarsPhotosRepository.getMarsPhotos() の呼び出しが成功した結果と一致していることを確認します。
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
   runTest {
       val marsViewModel = MarsViewModel(
           marsPhotosRepository = FakeNetworkMarsPhotosRepository()
       )
       assertEquals(
           MarsUiState.Success("Success: ${FakeDataSource.photosList.size} Mars " +
                   "photos retrieved"),
           marsViewModel.marsUiState
       )
   }

このテストをそのまま実行しようとすると、失敗します。エラーは次の例のようになります。

Exception in thread "Test worker @coroutine#1" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used

MarsViewModelviewModelScope.launch() を使用してリポジトリを呼び出すことを思い出してください。この命令は、デフォルトのコルーチン ディスパッチャ(Main ディスパッチャ)の下で、新しいコルーチンを開始します。Main ディスパッチャは、Android UI スレッドをラップします。前述のエラーの理由は、単体テストが Android UI スレッドに対応していないことです。単体テストは、Android デバイスやエミュレータではなく、ワークステーションで実行されます。ローカル単体テストのコードが Main ディスパッチャを参照する場合、単体テストの実行時に例外(上記の例など)がスローされます。この問題を解決するには、単体テストの実行時にデフォルトのディスパッチャを明示的に定義する必要があります。方法については、次のセクションをご覧ください。

テスト ディスパッチャを作成する

Main ディスパッチャは UI コンテキストでのみ使用できるため、ディスパッチャを単体テストに適したものに置き換える必要があります。Kotlin コルーチン ライブラリには、この目的のために TestDispatcher というコルーチン ディスパッチャが用意されています。新しいコルーチンが生成される単体テストでは、ビューモデルの getMarsPhotos() 関数の場合と同様に、Main ディスパッチャの代わりに TestDispatcher を使用する必要があります。

いずれの場合も、Main ディスパッチャを TestDispatcher に置き換えるには、Dispatchers.setMain() 関数を使用します。Dispatchers.resetMain() 関数を使用して、スレッド ディスパッチャを Main ディスパッチャにリセットできます。各テストで Main ディスパッチャを置き換えるコードが重複しないように、JUnit テストルールに抽出できます。TestRule は、テストを実行する環境を制御する方法を提供します。TestRule は、さらにチェックを追加したり、テストに必要なセットアップやクリーンアップを行ったり、別の場所で報告するためにテスト実行を監視したりできます。TestRule は、テストクラス間で簡単に共有できます。

Main ディスパッチャを置き換えるために、TestRule を記述する専用のクラスを作成します。カスタム TestRule を実装するには次の手順を行います。

  1. テスト ディレクトリに rules という新しいパッケージを作成します。
  2. rules ディレクトリで、TestDispatcherRule という新しいクラスを作成します。
  3. TestDispatcherRuleTestWatcher で拡張します。あTestWatcher クラスを使用すると、テストのさまざまな実行フェーズでアクションを実行できます。
class TestDispatcherRule(): TestWatcher(){

}
  1. TestDispatcherRuleTestDispatcher コンストラクタ パラメータを作成します。

このパラメータを使用すると、StandardTestDispatcher など、さまざまなディスパッチャを使用できます。このコンストラクタ パラメータには、デフォルト値として UnconfinedTestDispatcher オブジェクトのインスタンスを設定する必要があります。UnconfinedTestDispatcher クラスは TestDispatcher クラスを継承し、タスクが特定の順序で実行されないように指定します。この実行パターンは、コルーチンが自動的に処理されるため、単純なテストに適しています。UnconfinedTestDispatcher とは異なり、StandardTestDispatcher クラスを使用すると、コルーチンの実行を完全に制御できます。この方法は、手動のアプローチを必要とする複雑なテストに適していますが、この Codelab のテストには必要ありません。

class TestDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {

}
  1. このテストルールの主な目的は、テストの実行を開始する前に Main ディスパッチャをテスト ディスパッチャに置き換えることです。TestWatcher クラスの starting() 関数は、特定のテストが実行される前に実行されます。starting() 関数をオーバーライドします。
class TestDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        
    }
}
  1. Dispatchers.setMain() の呼び出しを追加し、引数として testDispatcher を渡します。
class TestDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }
}
  1. テスト実行が終了したら、finished() メソッドをオーバーライドして Main ディスパッチャをリセットします。Dispatchers.resetMain() 関数を呼び出します。
class TestDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

TestDispatcherRule ルールを再利用できるようになりました。

  1. MarsViewModelTest.kt ファイルを開きます。
  2. MarsViewModelTest クラスで、TestDispatcherRule クラスをインスタンス化し、testDispatcher 読み取り専用プロパティに割り当てます。
class MarsViewModelTest {
    
    val testDispatcher = TestDispatcherRule()
    ...
}
  1. このルールをテストに適用するには、testDispatcher プロパティに @get:Rule アノテーションを追加します。
class MarsViewModelTest {
    @get:Rule
    val testDispatcher = TestDispatcherRule()
    ...
}
  1. テストを再実行します。今回はテストに合格することを確認します。

11. 解答コードを取得する

この Codelab の完成したコードをダウンロードするには、以下のコマンドを使用します。

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout coil-starter

または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。

この Codelab の解答コードを確認する場合は、GitHub で表示します。

12. まとめ

おつかれさまでした。Codelab を完了し、Mars Photos アプリをリファクタリングして、リポジトリ パターンと依存関係インジェクションを実装しました。

アプリのコードは、データレイヤに関する Android のベスト プラクティスに従うようになりました。つまり、柔軟性、堅牢性が高くなり、拡張が容易になります。

またこれらの変更により、アプリを容易にテストできるようになりました。この利点は非常に重要です。コードが想定どおりに動作することを確認しながら、コードを改善し続けることができるためです。

作成したら、#AndroidBasics を付けて、ソーシャル メディアで共有しましょう。

13. 関連リンク

Android デベロッパー ドキュメント:

その他: