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 個以上のデータソースが含まれています。
ベスト プラクティスとして、アプリで使用するデータソースのタイプごとにリポジトリを提供する必要があります。
この Codelab では、アプリに 1 つのデータソースがあるため、コードをリファクタリングした後でアプリに 1 つのリポジトリが作成されます。このアプリの場合、インターネットからデータを取得するリポジトリが、データソースの役割を果たします。これは、API に対するネットワーク リクエストによって行われます。データソースのコーディングがより複雑になるか、新しいデータソースが追加される場合、データソースの責任はさまざまなデータソース クラスにカプセル化され、リポジトリはすべてのデータソースの管理を担当します。
リポジトリとは
リポジトリ クラスには通常、次のような役割があります。
- アプリの他の部分にデータを公開する。
- データの変更を一元管理する。
- 複数のデータソース間の競合を解決する。
- アプリの他の部分からデータソースを抽象化する。
- ビジネス ロジックを含む。
Mars Photos アプリには、ネットワーク API 呼び出しという 1 つのデータソースがあります。このアプリはデータを取得するだけなので、ビジネス ロジックを含みません。データはリポジトリ クラスを介してアプリに公開され、リポジトリ クラスはデータのソースを抽象化します。
3. データレイヤを作成する
まず、リポジトリ クラスを作成する必要があります。Android デベロッパー ガイドには、リポジトリ クラスの名前は、担当するデータに基づいて付けられると記載されています。リポジトリの命名規則は、「データのタイプ + リポジトリ」です。このアプリの場合は MarsPhotosRepository
になります。
リポジトリを作成する
- [com.example.marsphotos] を右クリックして [New] > [Package] を選択します。
- ダイアログで「
data
」と入力します。 data
パッケージを右クリックして、[New] > [Kotlin Class/File] を選択します。- ダイアログで [Interface] を選択し、インターフェースの名前として「
MarsPhotosRepository
」と入力します。 MarsPhotosRepository
インターフェース内にgetMarsPhotos()
という抽象関数を追加します。この関数はMarsPhoto
オブジェクトのリストを返します。呼び出しはコルーチンから行われるため、宣言にはsuspend
を使用します。
import com.example.marsphotos.model.MarsPhoto
interface MarsPhotosRepository {
suspend fun getMarsPhotos(): List<MarsPhoto>
}
- インターフェース宣言の下に、
NetworkMarsPhotosRepository
という名前のクラスを作成してMarsPhotosRepository
インターフェースを実装します。 - インターフェース
MarsPhotosRepository
をクラス宣言に追加します。
インターフェースの抽象メソッドをオーバーライドしていなかったため、エラー メッセージが表示されます。次の手順でこのエラーを解消します。
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
コードを更新する必要があります。
ui/screens/MarsViewModel.kt
ファイルを開きます。- 下にスクロールして
getMarsPhotos()
メソッドを表示します。 - 「
val listResult = MarsApi.retrofitService.getPhotos()
」という行を次のコードで置き換えます。
import com.example.marsphotos.data.NetworkMarsPhotosRepository
val marsPhotosRepository = NetworkMarsPhotosRepository()
val listResult = marsPhotosRepository.getMarsPhotos()
- アプリを実行します。お気づきのように、表示される結果は前の結果と同じです。
ViewModel
がデータのネットワーク リクエストを直接行うのではなく、リポジトリがデータを提供します。ViewModel
は MarsApi
コードを直接参照しなくなりました。
この方法により、データを取得するコードが 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 がテストに役立つ方法の一例として、ネットワーク呼び出しコードをテストするというものがあります。このテストでは、ネットワーク呼び出しが行われ、データが返されるかどうかを実際にテストしてみます。テスト中にネットワーク リクエストを行うたびに支払いが発生する場合は、コストが高くなる可能性があるため、このコードのテストをスキップできます。では、テスト用にネットワーク リクエストを模擬できる場合はどうでしょうか。どのくらいコストを削減できるでしょうか。テスト用に、ネットワーク呼び出しを実際に行わなくても、リポジトリにテスト オブジェクトを渡して、呼び出し時の架空のデータを返すことができます。
ViewModel
をテスト可能にしたいのですが、現在は実際のネットワーク呼び出しを行うリポジトリに依存しています。実際の本番環境リポジトリでテストすると、多くのネットワーク呼び出しが行われます。この問題を解決するには、ViewModel
がリポジトリを作成するのではなく、本番環境とテストに使用するリポジトリ インスタンスを動的に決定して渡す方法が必要です。
このプロセスは、リポジトリを MarsViewModel
に提供するアプリケーション コンテナを実装することで行われます。
コンテナは、アプリに必要な依存関係を含むオブジェクトです。これらの依存関係はアプリ全体で使用されるため、すべてのアクティビティが使用できる共通の場所に配置する必要があります。Application クラスのサブクラスを作成して、コンテナへの参照を格納できます。
アプリケーション コンテナを作成する
data
パッケージを右クリックして、[New] > [Kotlin Class/File] を選択します。- ダイアログで [Interface] を選択し、インターフェースの名前として「
AppContainer
」と入力します。 AppContainer
インターフェース内に、MarsPhotosRepository
型のmarsPhotosRepository
という抽象プロパティを追加します。- インターフェース定義の下に、インターフェース
AppContainer
を実装するDefaultAppContainer
というクラスを作成します。 network/MarsApiService.kt
から、変数BASE_URL
、retrofit
、retrofitService
のコードを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)
}
}
- 変数
BASE_URL
の場合、const
キーワードを削除します。BASE_URL
はトップレベル変数ではなくなり、DefaultAppContainer
クラスのプロパティになったため、const
を削除する必要があります。キャメルケースbaseUrl
にリファクタリングします。 - 変数
retrofitService
の場合は、private
可視性修飾子を追加します。private
修飾子を追加したのは、変数retrofitService
がプロパティmarsPhotosRepository
によってクラス内でのみ使用され、クラス外からアクセスできるようにする必要がないためです。 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)
}
}
data/MarsPhotosRepository.kt
ファイルを開きます。retrofitService
をNetworkMarsPhotosRepository
に渡しているため、NetworkMarsPhotosRepository
クラスを変更する必要があります。- 次のコードに示すように、
NetworkMarsPhotosRepository
クラス宣言にコンストラクタ パラメータmarsApiService
を追加します。
import com.example.marsphotos.network.MarsApiService
class NetworkMarsPhotosRepository(
private val marsApiService: MarsApiService
) : MarsPhotosRepository {
NetworkMarsPhotosRepository
クラスのgetMarsPhotos()
関数内の return 文を変更してmarsApiService
からデータを取得します。
override suspend fun getMarsPhotos(): List<MarsPhoto> = marsApiService.getPhotos()
}
MarsPhotosRepository.kt
ファイルから次のインポートを削除します。
// Remove
import com.example.marsphotos.network.MarsApi
network/MarsApiService.kt
ファイルのすべてのコードをオブジェクトから移動しました。残りのオブジェクト宣言は不要になったため、削除できます。
- 次のコードを削除します。
object MarsApi {
}
5. アプリケーション コンテナをアプリにアタッチする
このセクションの手順では、次の図に示すように、アプリのオブジェクトをアプリケーション コンテナに接続します。
com.example.marsphotos
を右クリックして、[New] > [Kotlin Class/File] を選択します。- ダイアログで「
MarsPhotosApplication
」と入力します。このクラスはアプリのオブジェクトから継承されるため、クラス宣言に追加する必要があります。
import android.app.Application
class MarsPhotosApplication : Application() {
}
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()
}
- 完全な
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()
}
}
- 定義したアプリクラスをアプリで使用できるように、Android マニフェストを更新する必要があります。
manifests/AndroidManifest.xml
ファイルを開きます。
application
セクションで、android:name
属性を追加し、アプリクラス名の値を".MarsPhotosApplication"
にします。
<application
android:name=".MarsPhotosApplication"
android:allowBackup="true"
...
</application>
6. ViewModel にリポジトリを追加する
これらの手順を完了すると、ViewModel
はリポジトリ オブジェクトを呼び出して火星のデータを取得できます。
ui/screens/MarsViewModel.kt
ファイルを開きます。MarsViewModel
のクラス宣言に、MarsPhotosRepository
型のプライベート コンストラクタ パラメータmarsPhotosRepository
を追加します。アプリが依存関係インジェクションを使用するようになったため、コンストラクタ パラメータの値はアプリケーション コンテナから取得されます。
import com.example.marsphotos.data.MarsPhotosRepository
class MarsViewModel(private val marsPhotosRepository: MarsPhotosRepository) : ViewModel(){
getMarsPhotos()
関数では、marsPhotosRepository
がコンストラクタ呼び出しに入力されるようになったため、次のコード行を削除します。
val marsPhotosRepository = NetworkMarsPhotosRepository()
- Android フレームワークでは、
ViewModel
を作成する際にコンストラクタで値を渡すことができません。この制限を回避するためにViewModelProvider.Factory
オブジェクトを実装します。
Factory パターンは、オブジェクトの作成に使用される作成パターンです。MarsViewModel.Factory
オブジェクトはアプリケーション コンテナを使用して marsPhotosRepository
を取得します。次に、ViewModel
オブジェクトの作成時にこのリポジトリを ViewModel
に渡します。
- 関数
getMarsPhotos()
の下に、コンパニオン オブジェクトのコードを入力します。
コンパニオン オブジェクトは、すべてのユーザーが使用するオブジェクトのインスタンスを 1 つ使用できる点で便利です。高価なオブジェクトのインスタンスを新たに作成する必要はありません。これは実装の詳細であり、分離することで、アプリのコードの他の部分に影響を与えずに変更を加えることができます。
APPLICATION_KEY
は ViewModelProvider.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)
}
}
}
theme/MarsPhotosApp.kt
ファイルを開き、MarsPhotosApp()
関数内で、Factory を使用するようviewModel()
を更新します。
Surface(
// ...
) {
val marsViewModel: MarsViewModel =
viewModel(factory = MarsViewModel.Factory)
// ...
}
この marsViewModel
変数は、viewModel()
関数への呼び出しによって入力されます。この関数は、引数としてコンパニオン オブジェクトの MarsViewModel.Factory
を渡されて ViewModel
を作成します。
- アプリを実行して、以前と同様に動作していることを確認します。
これで、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")
ローカルテスト ディレクトリを作成する
- プロジェクト ビューで src ディレクトリを右クリックし、[New] > [Directory] > [test/java] を選択してローカル テスト ディレクトリを作成します。
- テスト ディレクトリに
com.example.marsphotos
という名前の新しいパッケージを作成します。
8. テスト用の架空のデータと依存関係を作成する
このセクションでは、依存関係インジェクションがローカルテストの作成にどのように役立つかを説明します。Codelab の前半で、API サービスに依存するリポジトリを作成しました。次に、リポジトリに依存するように ViewModel
を変更しました。
各ローカルテストで 1 つの項目のみをテストします。たとえば、ビューモデルの機能をテストしても、リポジトリや API サービスの機能をテストしない場合があります。同様に、リポジトリをテストするときに、API サービスをテストしない場合があります。
インターフェースを使用し、次いで依存関係インジェクションを使用して、これらのインターフェースから継承するクラスを含めると、テスト用に作成された架空のクラスを使用して依存関係の機能をシミュレートできます。テスト用の架空のクラスとデータソースを挿入することで、再現性と整合性を保ちながらコードを個別にテストできます。
まず必要なのは、架空のデータを作成して、後で作成する架空のクラスで使用できるようにすることです。
- テスト ディレクトリで、
com.example.marsphotos
の下にfake
というパッケージを作成します。 fake
ディレクトリにFakeDataSource
という新しい Kotlin オブジェクトを作成します。- このオブジェクトで、
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 サービスのメソッドが呼び出されたときに架空のデータを受け取ります。
fake
パッケージで、FakeMarsApiService
という名前の新しいクラスを作成します。MarsApiService
インターフェースを継承するようにFakeMarsApiService
クラスをセットアップします。
class FakeMarsApiService : MarsApiService {
}
getPhotos()
関数をオーバーライドします。
override suspend fun getPhotos(): List<MarsPhoto> {
}
getPhotos()
メソッドから架空の写真のリストを返します。
override suspend fun getPhotos(): List<MarsPhoto> {
return FakeDataSource.photosList
}
なお、このクラスの目的がよくわからなくても問題ありません。この架空のクラスの使用法については、次のセクションで詳しく説明します。
9. リポジトリ テストを作成する
このセクションでは、NetworkMarsPhotosRepository
クラスの getMarsPhotos()
メソッドをテストします。さらに、架空のクラスの使用方法を説明し、コルーチンをテストする方法を示します。
- 架空のディレクトリに、
NetworkMarsRepositoryTest
という名前の新しいクラスを作成します。 - 作成したクラス内に
networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList()
という新しいメソッドを作成し、@Test
アノテーションを付けます。
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList(){
}
リポジトリをテストするには、NetworkMarsPhotosRepository
のインスタンスが必要になります。このクラスは MarsApiService
インターフェースに依存することを思い出してください。ここで、前のセクションの架空の API サービスを活用します。
NetworkMarsPhotosRepository
のインスタンスを作成し、FakeMarsApiService
をmarsApiService
パラメータとして渡します。
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList(){
val repository = NetworkMarsPhotosRepository(
marsApiService = FakeMarsApiService()
)
}
架空の API サービスを渡すことで、リポジトリ内の marsApiService
プロパティの呼び出しにより、FakeMarsApiService
が呼び出されます。依存関係に架空のクラスを渡すことで、依存関係が返す内容を正確に制御できます。この方法により、テスト対象のコードは、テストされていないコードや、変更の可能性や予期しない問題が発生する可能性がある API に依存しなくなります。このような状況では、作成したコードに問題がなくても、テストが失敗することがあります。架空の実装は、より一貫性のあるテスト環境の作成、テストの不安定性の低減、1 つの機能をテストする簡潔なテストに役立ちます。
getMarsPhotos()
メソッドから返されたデータがFakeDataSource.photosList
と等しいことを確認します。
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList(){
val repository = NetworkMarsPhotosRepository(
marsApiService = FakeMarsApiService()
)assertEquals(FakeDataSource.photosList, repository.getMarsPhotos())
}
IDE では、getMarsPhotos()
メソッド呼び出しに赤色の下線が表示されます。
メソッドにカーソルを合わせると、「suspend 関数「getMarsPhotos」をコルーチンまたは別の suspend 関数からのみ呼び出す必要があります」というツールチップが表示されます。
data/MarsPhotosRepository.kt
で、NetworkMarsPhotosRepository
の getMarsPhotos()
実装を見ると、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()
テストを変更します。
NetworkMarsRepositoryTest.kt
で、networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList()
関数を式に変更します。
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
- この式を
runTest()
関数と同じになるように設定します。このメソッドではラムダが想定されています。
...
import kotlinx.coroutines.test.runTest
...
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
runTest {}
コルーチン テスト ライブラリには runTest()
関数が用意されています。この関数は、ラムダで渡されたメソッドを受け取り、TestScope
(CoroutineScope
から継承)から実行します。
- テスト関数の内容をラムダ関数に移動します。
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
runTest {
val repository = NetworkMarsPhotosRepository(
marsApiService = FakeMarsApiService()
)
assertEquals(FakeDataSource.photosList, repository.getMarsPhotos())
}
getMarsPhotos()
の赤色の線が消えています。これを実行すると、テストに合格します。
10. ViewModel テストを作成する
このセクションでは、MarsViewModel
から getMarsPhotos()
関数のテストを作成します。MarsViewModel
は MarsPhotosRepository
に依存します。したがって、このテストを作成するには、架空の MarsPhotosRepository
を作成する必要があります。また、runTest()
メソッドを使用するだけでなく、コルーチンにとって考慮すべき追加の手順がいくつかあります。
架空のリポジトリを作成する
このステップの目的は、MarsPhotosRepository
インターフェースから継承した架空のクラスを作成し、getMarsPhotos()
関数をオーバーライドして架空のデータを返すことです。この方法は、架空の API サービスで採用する方法と似ていますが、このクラスで MarsApiService
ではなく MarsPhotosRepository
インターフェースを拡張する点が異なります。
fake
ディレクトリにFakeNetworkMarsPhotosRepository
という新しいクラスを作成します。MarsPhotosRepository
インターフェースを使用してこのクラスを拡張します。
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
}
getMarsPhotos()
関数をオーバーライドします。
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
override suspend fun getMarsPhotos(): List<MarsPhoto> {
}
}
getMarsPhotos()
関数からFakeDataSource.photosList
を返します。
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
override suspend fun getMarsPhotos(): List<MarsPhoto> {
return FakeDataSource.photosList
}
}
ViewModel テストを作成する
MarsViewModelTest
という新しいクラスを作成します。marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()
という関数を作成し、@Test
アノテーションを付けます。
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()
- この関数を
runTest()
メソッドの結果に設定される式にして、前のセクションのリポジトリ テストと同様に、コルーチンからテストを実行します。
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
runTest{
}
runTest()
のラムダ本体で、MarsViewModel
のインスタンスを作成し、作成した架空のリポジトリのインスタンスを渡します。
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
runTest{
val marsViewModel = MarsViewModel(
marsPhotosRepository = FakeNetworkMarsPhotosRepository()
)
}
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
MarsViewModel
が viewModelScope.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 を実装するには次の手順を行います。
- テスト ディレクトリに
rules
という新しいパッケージを作成します。 - rules ディレクトリで、
TestDispatcherRule
という新しいクラスを作成します。 TestDispatcherRule
をTestWatcher
で拡張します。あTestWatcher
クラスを使用すると、テストのさまざまな実行フェーズでアクションを実行できます。
class TestDispatcherRule(): TestWatcher(){
}
TestDispatcherRule
のTestDispatcher
コンストラクタ パラメータを作成します。
このパラメータを使用すると、StandardTestDispatcher
など、さまざまなディスパッチャを使用できます。このコンストラクタ パラメータには、デフォルト値として UnconfinedTestDispatcher
オブジェクトのインスタンスを設定する必要があります。UnconfinedTestDispatcher
クラスは TestDispatcher
クラスを継承し、タスクが特定の順序で実行されないように指定します。この実行パターンは、コルーチンが自動的に処理されるため、単純なテストに適しています。UnconfinedTestDispatcher
とは異なり、StandardTestDispatcher
クラスを使用すると、コルーチンの実行を完全に制御できます。この方法は、手動のアプローチを必要とする複雑なテストに適していますが、この Codelab のテストには必要ありません。
class TestDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
}
- このテストルールの主な目的は、テストの実行を開始する前に
Main
ディスパッチャをテスト ディスパッチャに置き換えることです。TestWatcher
クラスのstarting()
関数は、特定のテストが実行される前に実行されます。starting()
関数をオーバーライドします。
class TestDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
}
}
Dispatchers.setMain()
の呼び出しを追加し、引数としてtestDispatcher
を渡します。
class TestDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
}
- テスト実行が終了したら、
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
ルールを再利用できるようになりました。
MarsViewModelTest.kt
ファイルを開きます。MarsViewModelTest
クラスで、TestDispatcherRule
クラスをインスタンス化し、testDispatcher
読み取り専用プロパティに割り当てます。
class MarsViewModelTest {
val testDispatcher = TestDispatcherRule()
...
}
- このルールをテストに適用するには、
testDispatcher
プロパティに@get:Rule
アノテーションを追加します。
class MarsViewModelTest {
@get:Rule
val testDispatcher = TestDispatcherRule()
...
}
- テストを再実行します。今回はテストに合格することを確認します。
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 デベロッパー ドキュメント:
その他: