Using Dagger in your Android appの日本語訳
1.はじめに
このコードラボでは、Dependency Injection(DI)の重要性を学習し、大規模プロジェクトに対応する堅実で拡張可能なアプリケーションを作成します。Daggerは依存関係を管理するDIツールとして使用します。
依存性注入(DI)は、プログラミングで広く使用されている手法であり、Android開発に適しています。 DIの原則に従うことで、優れたアプリアーキテクチャの基礎を築きます。
依存性注入を実装すると、次の利点が得られます。
- コードの再利用性
- リファクタリングの容易さ
- テストのしやすさ
前提条件
- Kotlin構文の経験
- 依存性注入とAndroidアプリでDaggerを使用する利点を理解していること
依存性注入の詳細と、AndroidアプリでDaggerがどのように役立つかについては、こちらをご覧ください。
学ぶこと
- 大規模なAndroidアプリでDaggerを使用する方法
- より堅実で持続可能なアプリを作成するための関連するDaggerの概念
- Daggerサブコンポーネントが必要な理由とその使用方法
- Daggerを使用するアプリケーションでユニットテストとインストゥルメント化テストする方法
コードラボの終わりまでに、あなたはこのようなアプリケーションのグラフを作成し、テストしています
矢印はオブジェクト間の依存関係を表します。 これをアプリケーショングラフと呼びます。アプリのすべてのクラスとそれらの間の依存関係です。
読み続けて、その方法を学びましょう!
2.設定する
コードを入手する
GitHubからcodelabのコードを取得します。
$ git clone https://github.com/googlecodelabs/android-dagger
また、リポジトリをZipファイルとしてダウンロードできます。
ダウンロード
Android Studioを開きます
Android Studioをダウンロードする必要がある場合は、ここからダウンロードできます。
プロジェクトのセットアップ
このプロジェクトは、複数のGitHubブランチで構築されています。
- masterは、チェックアウトまたはダウンロードしたブランチです。 コードラボの出発点。
- 1_registration_main、2_subcomponents、および3_dagger_appは、ソリューションへの中間ステップです。
- ソリューションには、このコードラボのソリューションが含まれています。
masterブランチから始めて、自分のペースでステップごとにコードラボに従うことをお勧めします。
コードラボでは、プロジェクトに追加する必要があるコードのスニペットが表示されます。一部の場所では、明示的に記述されているコードやコードスニペットのコメントに含まれるコードも削除する必要があります。
チェックポイントとして、特定のステップで支援が必要な場合に利用できる中間ブランチがあります。
gitを使用してソリューションブランチを取得するには、次のコマンドを使用します。
$ git clone -b solution https://github.com/googlecodelabs/android-dagger
または、ここからソリューションコードをダウンロードできます。
ダウンロード
よくある質問
- Android Studioをインストールするにはどうすればよいですか?
- 開発用のデバイスを設定するにはどうすればよいですか?
3.サンプルアプリの実行
最初に、開始サンプルアプリの外観を見てみましょう。 以下の手順に従って、Android Studioでサンプルアプリを開きます。
- zipアーカイブをダウンロードした場合は、ファイルをローカルで解凍します
- Android Studioでプロジェクトを開きます
-
実行ボタンをクリックし、エミュレーターを選択するか、Androidデバイスを接続します。 登録画面が表示されます
アプリは4つの異なるフローで構成されます(アクティビティとして実装)
- 登録:ユーザーは、ユーザー名、パスワードを紹介し、利用規約に同意することで登録できます
- ログイン:ユーザーは、登録フロー中に導入された資格情報を使用してログインでき、アプリから登録解除することもできます
- ホーム:ユーザーは歓迎され、未読通知の数を確認できます
- 設定:ユーザーはログアウトして未読の通知の数を更新できます(ランダムな数の通知が生成されます)
このプロジェクトは、Viewのすべての複雑さがViewModelに委ねられる典型的なMVVMパターンに従います。 時間をかけてプロジェクトの構造を理解してください。
矢印はオブジェクト間の依存関係を表します。 これをアプリケーショングラフと呼びます。アプリのすべてのクラスとそれらの間の依存関係です。
masterブランチのコードは、依存関係を手動で管理します。 手作業で作成する代わりに、Daggerを使用して管理するためにアプリをリファクタリングします。
免責事項
このコードラボは、アプリの設計方法については述べていません。Daggerをアプリアーキテクチャにプラグインできるさまざまな方法を紹介することを目的としています。複数のフラグメントを持つ単一のアクティビティ(登録およびログインフロー)または複数のアクティビティ(メインアプリフロー)。
コードラボを完了してDaggerの主な概念を理解し、それに応じてプロジェクトに適用できます。 このコードラボで使用されている一部のパターンは、Androidアプリケーションを構築するための推奨される方法ではありませんが、Daggerを説明するのに最適なパターンです。
Androidアプリアーキテクチャの詳細については、アプリアーキテクチャガイドページをご覧ください。
なぜDaggerなのか?
アプリケーションが大きくなると、エラーが発生する可能性のあるボイラープレートコード(ファクトリーなど)の記述を開始します。 これを間違えると、アプリで微妙なバグやメモリリークが発生する可能性があります。
コードラボでは、Daggerを使用してこのプロセスを自動化し、他の方法で手書きで記述したのと同じコードを生成する方法を説明します。
Daggerがアプリケーショングラフの作成を担当します。 また、依存関係を手動で作成する代わりに、アクティビティでフィールドインジェクションを実行するためにDaggerを使用します。
なぜDaggerなのかの詳細はこちら。
4. Daggerをプロジェクトに追加する
Daggerをプロジェクトに追加するには、app/build.gradleファイルを開き、ファイルの上部に2つのDagger依存関係とkaptプラグインを追加します。
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
...
dependencies {
...
def dagger_version = "2.25.2"
implementation "com.google.dagger:dagger:$dagger_version"
kapt "com.google.dagger:dagger-compiler:$dagger_version"
}
これらの行をファイルに追加したら、ファイルの上部に表示される[Sync Now]ボタンをクリックします。 これにより、プロジェクトが同期され、新しい依存関係がダウンロードされます。 これでアプリでDaggerを使用する準備が整いました。
Daggerは、Javaのアノテーションモデルを使用して実装されます。 アノテーションプロセッサを使用して、コンパイル時にコードを生成します。アノテーションプロセッサは、kaptコンパイラプラグインを使用してKotlinでサポートされています。これは、ファイル上部の[apply plugin: 'kotlin-android-extensions']行の下のに[apply plugin: 'kotlin-kapt']を追加することで有効になります。
依存関係では、daggerライブラリにはアプリで使用できるすべてのアノテーションが含まれており、dagger-compilerはコードを生成するアノテーションプロセッサです。後者はアプリに入っていません。
Daggerの利用可能な最新バージョンは、こちらで見つけることができます。
5. @Injectアノテーション
Daggerを使用するために登録フローのリファクタリングを始めましょう。
アプリケーショングラフを自動的に作成するために、Daggerはグラフ内のクラスのインスタンスを作成する方法を知る必要があります。これを行う1つの方法は、クラスのコンストラクターに@Injectアノテーションを付けることです。コンストラクターのパラメーターは、そのタイプの依存関係になります。
RegistrationViewModel.ktファイルを開き、クラス定義を次のものに置き換えます。
// @Injectは、この型のインスタンスを提供する方法をDaggerに伝えます
// Daggerは、UserManagerが依存関係であることも知っています
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
...
}
Kotlinでは、コンストラクターにアノテーションを適用するには、上記のスニペットコードに示すように、キーワードコンストラクターを具体的に追加し、その直前にアノテーションを導入する必要があります。
@Injectアノテーションにより、Daggerは次のことを認識します。
- RegistrationViewModel型のインスタンスを作成する方法。
- コンストラクターは引数としてUserManagerのインスタンスを取るため、RegistrationViewModelには依存関係としてUserManagerがあります。
簡単にするために、RegistrationViewModelはAndroidアーキテクチャコンポーネントのViewModelではありません。 ViewModelとして機能するのは単なる通常のクラスです。
Daggerでこれを使用する方法の詳細については、公式のAndroid Blueprints codeの実装をご覧ください。
DaggerはまだUserManagerを作成タイプの方法を知りません。 同じプロセスに従って、@InjectアノテーションをUserManagerのコンストラクターに追加します。
UserManager.ktファイルを開き、クラス定義を次のものに置き換えます。
class UserManager @Inject constructor(private val storage: Storage) {
...
}
これで、DaggerはRegistrationViewModelおよびUserManagerのインスタンスを提供する方法を認識しました。
UserManagerの依存関係(つまり、ストレージ)はインターフェイスであるため、別の方法でそのインスタンスを作成する方法をDaggerに伝える必要があります。これについては後で説明します。
ビューにはグラフのオブジェクトが必要です
アクティビティやフラグメントなどの特定のAndroidフレームワーククラスはシステムによってインスタンス化されるため、Daggerはそれらを作成できません。特にアクティビティの場合、初期化コードはonCreateメソッドに移動する必要があります。そのため、以前のようにViewクラスのコンストラクターで@Injectアノテーションを使用することはできません(コンストラクターインジェクションと呼ばれます)。代わりに、フィールドインジェクションを使用する必要があります。
手動の依存関係注入のように、アクティビティがonCreateメソッドで必要とする依存関係を作成する代わりに、Daggerにこれらの依存関係を設定してもらいます。フィールドインジェクション(アクティビティとフラグメントで一般的に使用される)の場合、Daggerに提供するフィールドに@Injectアノテーションを付けます。
このアプリでは、RegistrationActivityはRegistrationViewModelに依存しています。
RegistrationActivity.ktを開くと、supportFragmentManagerを呼び出す直前にonCreateメソッドでViewModelが作成されます。
手作業で作成するのではなく、Daggerが提供するようにします。 そのためには、次のことが必要です。
- フィールドに@Injectアノテーションを付けます
- onCreateメソッドからインスタンス化を削除します
class RegistrationActivity : AppCompatActivity() {
// @Inject annotated fields will be provided by Dagger
@Inject
lateinit var registrationViewModel: RegistrationViewModel
override fun onCreate(savedInstanceState: Bundle?) {
...
// Remove following line
registrationViewModel = RegistrationViewModel((application as MyApplication).userManager)
}
}
クラスコンストラクターで@Injectアノテーションが付いている場合、そのクラスのインスタンスを提供する方法をDaggerに伝えています。クラスフィールドにアノテーションを付けると、そのタイプのインスタンスをフィールドに入力する必要があることをDaggerに伝えます。
どのオブジェクトをそのRegistrationActivityに注入する必要があるかをDaggerに伝えるにはどうすればよいですか?Daggerグラフ(またはアプリケーショングラフ)を作成し、それを使用してオブジェクトをアクティビティに注入する必要があります。
6. @Componentアノテーション
Daggerにプロジェクトの依存関係のグラフを作成し、管理してもらい、グラフから依存関係を取得できるようにします。Daggerで実行するには、インターフェイスを作成し、@Componentアノテーションを付ける必要があります。Daggerは、手動の依存関係注入で行ったように、コンテナを作成します。
@Componentは、Daggerが公開するメソッドのパラメーターを満たすために必要なすべての依存関係を持つコードを生成します。そのインターフェース内で、RegistrationActivityがインジェクションを要求していることをDaggerに伝えることができます。
package com.example.android.dagger.di
import com.example.android.dagger.registration.RegistrationActivity
import dagger.Component
// Daggerコンポーネントの定義
@Component
interface AppComponent {
// このコンポーネントによって注入できるクラス
fun inject(activity: RegistrationActivity)
}
@Componentインターフェースのinject(activity: RegistrationActivity)メソッドを使用して、RegistrationActivityがインジェクションを要求し、Activityがインジェクトするものを提供する必要があることをDaggerに伝えています(つまり、前のステップで定義したRegistrationViewModel)。
DaggerはRegistrationViewModelのインスタンスを内部で作成する必要があるため、RegistrationViewModelの依存関係(つまりUserManager)を満たす必要もあります。Daggerが提供する必要があるオブジェクトの依存関係を見つけるこの再帰プロセス中に、特定の依存関係を提供する方法がわからない場合、コンパイル時に、満たすことができない依存関係があると言って失敗します。
@Componentインターフェイスは、Daggerがコンパイル時にグラフを生成するために必要な情報を提供します。インターフェイスメソッドのパラメーターは、どのクラスがインジェクションを要求するかを定義します。
アプリをビルドすると、依存関係を管理するために必要なコードを生成するDaggerの注釈プロセッサがトリガーされます。 Android Studioのビルドボタンを使用してそれを行うと、次のエラーが表示されます。
dagger/app/build/tmp/kapt3/stubs/debug/com/example/android/dagger/di/AppComponent.java:7: error: [Dagger/MissingBinding] com.example.android.dagger.storage.Storage cannot be provided without an @Provides-annotated method
分解してみましょう。まず、AppComponentでエラーが発生していることを示しています。エラーのタイプは[Dagger/MissingBinding]です。これは、Daggerが特定のタイプを提供する方法を知らないことを意味します。読み続けると、@Provides注釈付きメソッドがないとストレージを提供できないと言われます。
Storage型のオブジェクトを提供する方法をDaggerに伝えるのを忘れていました!
7. @Module、@Bindsおよび@BindsInstanceアノテーション
Storageはインターフェースであるため、Daggerにストレージを提供する方法を指定する方法は異なります。使用するストレージの実装をDaggerに伝える必要があります: SharedPreferencesStorage
型のインスタンスを提供する方法をDaggerに伝える別の方法は、Daggerモジュールの情報を使用します。ダガーモジュールは、@Moduleアノテーションが付けられたクラスです。そこで、@Providesまたは@Bindsアノテーションを使用して依存関係を提供する方法を定義できます。
@Providesの詳細については、Androidアプリのドキュメントでダガーを使用するか、このコードラボの最後にあります。
このモジュールにはストレージに関する情報が含まれるので、AppComponent.ktを作成したのと同じパッケージにStorageModule.ktという別のファイルを作成しましょう。 そのファイルでは、@ Moduleアノテーションが付けられたStorageModuleというクラスを定義します。
package com.example.android.dagger.di
import dagger.Module
// ダガーモジュールであることをダガーに伝える
@Module
class StorageModule {
}
@Bindsアノテーション
@Bindsを使用して、インターフェイスを提供するときに使用する必要のある実装をDaggerに伝えます。
@Bindsは抽象関数に注釈を付ける必要があります(抽象であるため、コードが含まれておらず、クラスも抽象である必要があるため)。抽象関数の戻り値の型は、実装(つまりストレージ)を提供するインターフェイスです。実装は、インターフェイス実装タイプ(つまり、SharedPreferencesStorage)を持つ一意のパラメーターを追加することによって指定されます。
// ダガーモジュールであることをダガーに伝える
// @Bindsのため、StorageModuleは抽象クラスである必要があります
@Module
abstract class StorageModule {
// ストレージ型が要求されたときにDaggerがSharedPreferencesStorageを提供するようにします
@Binds
abstract fun provideStorage(storage: SharedPreferencesStorage): Storage
}
上記のコードを使用して、StorageインターフェイスをSharedPreferencesStorageの実装にリンクしました。現在StorageModuleは抽象的であることに注意してください。
モジュールは、セマンティックな方法でオブジェクトの提供の仕方をカプセル化する方法です。ご覧のとおり、ストレージモジュールを呼び出して、ストレージに関連するオブジェクトを提供するロジックをグループ化しました。ここでアプリケーションが拡張または複雑になった場合、たとえば、SharedPreferencesのさまざまな実装を提供する方法もここに含めることができます。
DaggerはSharedPreferencesStorageのインスタンスを作成する方法をまだ知りません。 以前と同じように、SharedPreferencesStorageのコンストラクターに@Injectアノテーションを付けます。
// @Injectは、この型のインスタンスを提供する方法をDaggerに伝えます
class SharedPreferencesStorage @Inject constructor(context: Context) : Storage { ... }
アプリケーショングラフは、StorageModuleについて知る必要があります。 そのため、次のようにAppComponentの@Componentアノテーション内にmodulesパラメーターを含めます。
// StorageModuleからの情報をグラフに追加するDaggerコンポーネントの定義
@Component(modules = [StorageModule::class])
interface AppComponent {
// このコンポーネントによって注入できるクラス
fun inject(activity: RegistrationActivity)
}
このようにして、AppComponentはStorageModuleに含まれる情報にアクセスできます。より複雑なアプリケーションでは、OkHttpClientを提供する方法や、GsonやMoshiを構成する方法などの情報を追加するNetworkModuleを使用することもできます。
再度ビルドしようとすると、前と同じようなエラーが表示されます! 今回Daggerが見つけられないのは、コンテキストです。
@BindsInstanceアノテーション
DaggerにContextの提供方法を伝えるにはどうすればよいですか?ContextはAndroidシステムによって提供されるため、グラフの外部で構築されます。Contextはグラフのインスタンスを作成する時点ですでに利用可能であるため、渡すことができます。
渡す方法は、コンポーネントファクトリを使用し、@BindsInstanceアノテーションを使用することです。
@Component(modules = [StorageModule::class])
interface AppComponent {
// AppComponentのインスタンスを作成するファクトリー
@Component.Factory
interface Factory {
// @BindsInstanceを使用すると、渡されたコンテキストはグラフで利用可能になります
fun create(@BindsInstance context: Context): AppComponent
}
fun inject(activity: RegistrationActivity)
}
@Component.Factoryアノテーションが付けられたFactoryというインターフェースを宣言しています。内部には、コンポーネント型(つまり、AppComponent)を返すメソッドがあり、@BindsInstanceアノテーションが付けられたContext型のパラメーターがあります。
@BindsInstanceはDaggerに、そのインスタンスをグラフに追加する必要があること、およびコンテキストが必要な場合は常にそのインスタンスを提供することを伝えます。
@BindsInstanceは、グラフの外部で構築されたオブジェクト(Contextのインスタンスなど)に使用します。
プロジェクトは正常にビルドされます
ここでプロジェクトをビルドすると、すべてが緑色でエラーが表示されないことがわかります。これは、Daggerがグラフを正常に生成し、すぐに使用する準備ができたことを意味します。
アプリケーショングラフの実装は、アノテーションプロセッサによって自動的に生成されます。生成されたクラスはDagger{ComponentName}と呼ばれ、グラフの実装が含まれます。次のセクションでは、生成されたDaggerAppComponentクラスを使用します。
AppComponentグラフはどのようになりましたか?
AppComponentには、Storageインスタンスを提供する方法に関する情報を含むStorageModuleが含まれています。StorageはContextに依存していますが、グラフを作成するときにStorageを提供しているため、Storageはすべての依存関係をカバーしています。
Contextのインスタンスは、AppComponentファクトリのcreateメソッドに渡されます。したがって、オブジェクトが"Context"を必要とするときはいつでも同じインスタンスが提供されます。これは、ダイアグラムの白い点で表されています。
これで、RegistrationActivityはグラフにアクセスして、Dagger(この場合はRegistrationViewModel)によって挿入(または入力)されたオブジェクトを取得できます。
AppComponentは、RegistrationViewModelのインスタンスを作成できるように、RegistrationActivityのRegistrationViewModelを設定する必要があるため、依存関係(つまり、UserManager)を満たし、UserManagerのインスタンスも作成する必要があります。UserManagerには*@Injectアノテーションが付けられたコンストラクターがあるため、Daggerはそれを使用してインスタンスを作成します。UserManagerはStorage*に依存していますが、すでにグラフにあるため、他に何も必要ありません。
8.アクティビティへのグラフの挿入
Androidでは、アプリが実行されている限りグラフのインスタンスをメモリに保持するため、通常はApplicationクラスに存在するDaggerグラフを作成します。このようにして、グラフはアプリのライフサイクルに付属されます。この場合、グラフでアプリケーションのコンテキストを使用できるようにすることも必要です。利点として、グラフは他のAndroidフレームワーククラスで利用可能です(Contextでアクセスできます)また、テストでカスタムApplicationクラスを使用できるため、テストにも適しています。
グラフのインスタンス(つまりAppComponent)をカスタムアプリケーションMyApplicationに追加しましょう。
open class MyApplication : Application() {
// プロジェクト内のすべてのアクティビティで使用されるAppComponentのインスタンス
val appComponent: AppComponent by lazy {
// Factoryコンストラクターを使用してAppComponentのインスタンスを作成します
// グラフのコンテキストとして使用されるapplicationContextを渡します
DaggerAppComponent.factory().create(applicationContext)
}
open val userManager by lazy {
UserManager(SharedPreferencesStorage(this))
}
}
前のセクションで述べたように、Daggerは、プロジェクトをビルドしたときにAppComponentグラフの実装を含むDaggerAppComponentというクラスを生成しました。@Component.Factoryアノテーションでコンポーネントファクトリを定義したため、DaggerAppComponentの静的メソッドである*.factory()を呼び出すことができます。これで、Context(この場合はapplicationContext*)を渡すファクトリ内で定義したcreateメソッドを呼び出すことができます。
変数が不変であり、必要な場合にのみ初期化されるように、Kotlin lazy initializationを使用してこれを行います。
注:DaggerAppComponentがプロジェクトに存在しないというエラーが表示された場合。 Daggerアノテーションプロセッサがコードを生成できるようにプロジェクトをビルドする必要があります。
新しいDaggerコードを有効にするには、常にプロジェクトをビルドする必要があります。
RegistrationActivityのグラフのthisインスタンスを使用して、Daggerに*@Injectアノテーションが付けられたフィールドを挿入させることができます。どうすればできますか?RegistrationActivityをパラメーターとして使用するAppComponent*のメソッドを呼び出す必要があります。
class RegistrationActivity : AppCompatActivity() {
// @Inject annotated fields will be provided by Dagger
@Inject
lateinit var registrationViewModel: RegistrationViewModel
override fun onCreate(savedInstanceState: Bundle?) {
// アプリケーショングラフのインスタンスを取得します
// @Injectフィールドにグラフのオブジェクトを入力します
(application as MyApplication).appComponent.inject(this)
super.onCreate(savedInstanceState)
...
}
...
}
appComponent.inject(this)を呼び出すと、RegistrationActivityが@Inject(つまりregistrationViewModel)でアノテーションを付けたフィールドにデータが入力されます。
重要:アクティビティを使用する場合は、フラグメントの復元に関する問題を回避するために、アクティビティのonCreateメソッドでsuper.onCreateを呼び出す前にDaggerを注入します。super.onCreateで、復元フェーズ中のアクティビティは、アクティビティバインディングにアクセスしたいフラグメントをアタッチします。
RegistrationActivityはすでにDaggerを使用して依存関係を管理しています! 先に進み、アプリを実行できます。
バグを見つけましたか?登録フローの後にメインページが表示されます!しかし、そうではない。ログインでもそうです。
どうしてですか?アプリの他のフローでは、Daggerグラフをまだ使用していません。
メインフローでもDaggerを使用して、この問題を修正しましょう。
メインフローでのDaggerの使用
前回と同様に、MainActivityでDaggerを使用して依存関係を管理する必要があります。 この場合、MainViewModelおよびUserManagerです。
MainActivityがインジェクションを要求していることをDaggerに伝えるには、AppComponentのインターフェイスのパラメーターとしてMainActivityを含む別の関数を追加する必要があります。
@Component(modules = [StorageModule::class])
interface AppComponent {
...
// *this*コンポーネントによって注入できるクラス
fun inject(activity: RegistrationActivity)
fun inject(activity: MainActivity)
}
関数の名前は重要ではありません(そのため、両方の関数をinjectと呼んでいます)。重要なのはパラメーターの型です。MainActivityでDaggerから注入するものを定義し、グラフを注入しましょう。
1.userManagerとmainViewModelの両方のフィールドに*@Inject*でアノテーションを付けます。
class MainActivity : AppCompatActivity() {
// *@Inject*アノテーション付きフィールドはDaggerによって提供されます
@Inject
private lateinit var userManager: UserManager
@Inject
private lateinit var mainViewModel: MainViewModel
...
}
2.Daggerによって行われるため、userManagerおよびmainViewModelの初期化を削除します。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
// ここを削除
userManager = (application as MyApplication).userManager
if (!userManager.isUserLoggedIn()) {
...
} else {
...
// ここも削除
mainViewModel = MainViewModel(userManager.userDataRepository!!)
...
}
}
...
}
3.appComponentにMainActivityを挿入して、挿入されたフィールドに入力します。
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
(application as MyApplication).appComponent.inject(this)
super.onCreate(savedInstanceState)
...
}
}
UserManagerはすでにグラフで利用できるため、Daggerは提供方法を知っていますが、MainViewModelは利用できません。@Injectアノテーションをコンストラクターに追加して、Daggerがクラスのインスタンスの作成方法を認識できるようにします。
class MainViewModel @Inject constructor(private val userDataRepository: UserDataRepository) { ... }
MainViewModelはUserDataRepositoryに依存しているため、@Injectのアノテーションを付ける必要があります。
class UserDataRepository @Inject constructor(private val userManager: UserManager) { ... }
UserManagerはすでにグラフの一部であるため、Daggerはグラフを正常に作成するために必要なすべての情報を持っています。
この時点でのアプリケーショングラフの現在の状態
プロジェクトを再度ビルドしようとすると、別のエラーが発生するはずです。
それは何と言っていますか?エラー:Daggerはプライベートフィールドへの挿入をサポートしていません。 これはDaggerの欠点の1つです。挿入されたフィールドには、少なくともパッケージプライベート以上の可視性が必要です。それらはクラスのプライベートにすることはできません。
フィールド定義からprivate修飾子を削除します。
class MainActivity : AppCompatActivity() {
@Inject
lateinit var userManager: UserManager
@Inject
lateinit var mainViewModel: MainViewModel
...
}
重要:Dagger注入フィールドはプライベートにすることはできません。 少なくともパッケージプライベートの可視性が必要です。
これで、プロジェクトを正常にビルドできます。アプリを再度実行してみましょう。再度実行すると、以前にユーザーを登録したため、ログイン画面が表示されます。初めてアプリケーションを実行したときに新たに開始するには、「登録解除」をクリックして登録フローに移動します。
登録すると、メインページには移動しません!ログインアクティビティに再び行きます。 バグが再び発生していますが、なぜですか?メインフローと登録フローの両方が、アプリケーショングラフからUserManagerを挿入しています。
問題は、デフォルトで依存関係を注入するときに、Daggerが常に型の新しいインスタンスを提供することです。毎回同じインスタンスを再利用するようにDaggerを作成するにはどうすればよいですか?スコープ付きにすることです。
9.スコープの使用
場合によっては、複数の理由により、コンポーネントの依存関係の同じインスタンスを提供することがあります。
- 依存関係としてこのタイプを持つ他のタイプが同じインスタンスを共有するようにします(例ではUserManager)。
- オブジェクトの作成は非常に高価であり、依存関係(Jsonパーサーなど)として宣言されるたびに新しいインスタンスを作成する必要はありません。
スコープを使用して、コンポーネント内の型の一意のインスタンスを保持します。これは、「コンポーネントのライフサイクルに型をスコープする」とも呼ばれます。コンポーネントに型をスコープするということは、型を提供する必要があるたびに、その型の同じインスタンスが使用されることを意味します。
AppComponentには、javax.injectパッケージに付属する唯一のスコープアノテーションである*@Singletonスコープアノテーションを使用できます。@Singletonを使用してコンポーネントにアノテーションを付けると、アノテーションが付けられたすべてのクラスは、アノテーションが付けられたコンポーネントにスコープされます。
AppComponent.ktファイルを開き、コンポーネントに@Singleton*のアノテーショ」ンを付けます。
@Singleton
@Component(modules = [StorageModule::class])
interface AppComponent { ... }
これで、@Singletonアノテーションが付けられたクラスのスコープがAppComponentになります。UserManagerにアノテーショ」ンを付けて、アプリケーショングラフに一意のインスタンスを持たせましょう。
@Singleton
class UserManager @Inject constructor(private val storage: Storage) {
...
}
これで、UserManagerの同じインスタンスがRegistrationActivityとMainActivityに提供されます。
アプリを再度実行し、登録フローに移動して、以前と同様に新たに開始します。 これで、登録が完了すると、メインフローに移動します! 問題が解決しました。
この時点でのコードラボのソリューションは、Githubプロジェクトの1_registration_mainブランチにあります。
興味がある場合は、アプリケーショングラフがどのように見えるかです。
AppComponentのUserManagerの一意のインスタンスを持つグラフの現在の状態
UserManagerにも白い点が付いていることに注意してください。Contextで発生したように、AppComponentの同じインスタンスの依存関係として必要な場合、そのUserManagerの同じインスタンスが常に提供されます。
10.サブコンポーネント
登録フロー
アプリをDaggerにリファクタリングし続けましょう。登録フラグメントはまだ手動の依存性注入を使用しています。それらを今すぐ移行しましょう。
両方のフラグメントをDaggerによって注入する必要があるため、AppComponentインターフェイスに追加してDaggerに知らせる必要があります。
@Singleton
@Component(modules = [StorageModule::class])
interface AppComponent {
...
fun inject(activity: RegistrationActivity)
fun inject(fragment: EnterDetailsFragment)
fun inject(fragment: TermsAndConditionsFragment)
fun inject(activity: MainActivity)
}
Daggerに提供するフィールドはどれですか?+EnterDetailsFragmentでは、Daggerに両方のViewModelを設定します。それには、フィールドに@Inject*アノテーションを付け、プライベートの可視性修飾子を削除します。
class EnterDetailsFragment : Fragment() {
@Inject
lateinit var registrationViewModel: RegistrationViewModel
@Inject
lateinit var enterDetailsViewModel: EnterDetailsViewModel
...
}
But we also have to remove the manual instantiations we have in the code. Remove the following lines:
class EnterDetailsFragment : Fragment() {
override fun onCreateView(...): View? {
...
// 次の行を削除
registrationViewModel = (activity as RegistrationActivity).registrationViewModel
enterDetailsViewModel = EnterDetailsViewModel()
...
}
}
これで、ApplicationクラスのappComponentインスタンスを使用して、フラグメントを注入できます。フラグメントの場合、super.onAttachを呼び出した後、onAttachメソッドを使用してコンポーネントを注入します。
class EnterDetailsFragment : Fragment() {
override fun onAttach(context: Context) {
super.onAttach(context)
(activity!!.application as MyApplication).appComponent.inject(this)
}
}
重要 - ベストプラクティス
アクティビティは、onCreateメソッドでsuperを呼び出す前にDaggerを注入します。
フラグメントは、onAttachメソッドでsuperを呼び出した後にDaggerを注入します。
Daggerが知る必要があるのは、EnterDetailsViewModelのインスタンスを提供する方法です。そのためには、コンストラクタに*@Injectアノテーションを付けます。RegistrationViewModelにはすでに@Inject*アノテーションが付けられており、RegistrationActivityにはそれが必要でした。
class EnterDetailsViewModel @Inject constructor() { ... }
EnterDetailsFragmentの準備ができました。TermsAndConditionsFragmentでも同じようにする必要があります。
- Daggerに提供するフィールド(registrationViewModel)に*@Inject*アノテーションを付け、プライベート可視性修飾子を削除します。
- 手動の依存性注入に必要なregistrationViewModelインスタンスを削除します。
- onAttachメソッドにDaggerを注入します。
class TermsAndConditionsFragment : Fragment() {
@Inject
lateinit var registrationViewModel: RegistrationViewModel
override fun onAttach(context: Context) {
super.onAttach(context)
(activity!!.application as MyApplication).appComponent.inject(this)
}
override fun onCreateView(...): View? {
...
// Remove following line
registrationViewModel = (activity as RegistrationActivity).registrationViewModel
...
}
}
これで、アプリを実行できます。
何が起こった?クラッシュしました!問題は、RegistrationViewModelの異なるインスタンスがRegistrationActivity、EnterDetailsFragment、およびTermsAndConditionsFragmentに挿入されていることです。しかし、それは私たちが望むものではありません。ActivityとFragmentsに同じインスタンスを注入する必要があります。
RegistrationViewModelに*@Singleton*で注釈を付けるとどうなりますか? それは今のところ問題を解決するでしょうが、将来的には問題を引き起こすでしょう
- 登録フローが終了した後、RegistrationViewModelのインスタンスが常にメモリにあることは望ましくありません。
- 登録フローごとにRegistrationViewModelの異なるインスタンスが必要です。ユーザーが登録および登録解除する場合、以前の登録データが存在することは望ましくありません。
登録フラグメントには、アクティビティから同じViewModelを再利用する必要がありますが、アクティビティが変更された場合、別のインスタンスが必要です。RegistrationViewModelをRegistrationActivityにスコープする必要があります。そのために、登録フローの新しいコンポーネントを作成し、ViewModelをその新しい登録コンポーネントにスコープすることができます。Daggerのsubcomponentsを使用します。
Dagger Subcomponents
RegistrationViewModelはUserRepositoryに依存するため、RegistrationComponentはAppComponentからオブジェクトにアクセスできる必要があります。新しいコンポーネントが別のコンポーネントの一部を使用することをDaggerに伝える方法は、Dagger Subcomponentsを使用することです。新しいコンポーネント(RegistrationComponent)は、共有リソースを含むコンポーネント(つまりAppComponent)のサブコンポーネントである必要があります。
Subcomponentsは、親コンポーネントのオブジェクトグラフを継承および拡張するコンポーネントです。したがって、親コンポーネントで提供されるすべてのオブジェクトは、サブコンポーネントでも提供されます。このようにして、サブコンポーネントのオブジェクトは、親コンポーネントによって提供されるオブジェクトに依存できます。
これは登録に固有のものであるため、登録パッケージ内に新しいRegistrationComponent.ktファイルを作成します。ここで、これがサブコンポーネントであることをDaggerに伝える*@Subcomponentアノテーションが付いたRegistrationComponent*という新しいインターフェイスを作成できます。
package com.example.android.dagger.registration
import dagger.Subcomponent
// Definition of a Dagger subcomponent
@Subcomponent
interface RegistrationComponent {
}
このコンポーネントには、登録固有の情報を含める必要があります。 そのためには、次のことが必要です。
- 登録固有のAppComponentから注入メソッドを追加します。RegistrationActivity、EnterDetailsFragment、およびTermsAndConditionsFragment。
- このサブコンポーネントのインスタンスを作成するために使用できるサブコンポーネントファクトリを作成します。
// Daggerサブコンポーネントの定義
@Subcomponent
interface RegistrationComponent {
// RegistrationComponentのインスタンスを作成するファクトリー
@Subcomponent.Factory
interface Factory {
fun create(): RegistrationComponent
}
// Classes that can be injected by this Component
fun inject(activity: RegistrationActivity)
fun inject(fragment: EnterDetailsFragment)
fun inject(fragment: TermsAndConditionsFragment)
}
AppComponentでは、登録ビュークラスを使用できなくなるため、登録ビュークラスを挿入できるメソッドを削除する必要があります。これらのクラスはRegistrationComponentを使用します。
代わりに、RegistrationActivityがRegistrationComponentのインスタンスを作成するには、AppComponentインターフェイスでファクトリを公開する必要があります。
@Singleton
@Component(modules = [StorageModule::class])
interface AppComponent {
@Component.Factory
interface Factory {
fun create(@BindsInstance context: Context): AppComponent
}
// グラフから登録コンポーネントファクトリを公開する
fun registrationComponent(): RegistrationComponent.Factory
fun inject(activity: MainActivity)
}
戻り値の型としてそのクラスを使用して関数を宣言することにより、RegistrationComponentファクトリを公開します。
ダガーグラフを操作するには、2つの異なる方法があります。]
- Unitを返し、クラスをパラメーターとして受け取る関数を宣言すると、そのクラスでのフィールドインジェクションが許可されます(例:fun inject(activity: MainActivity))。
- 型を返す関数を宣言すると、グラフから型を取得できます(例:fun registrationComponent(): RegistrationComponent.Factory)。
次に、RegistrationComponentがサブコンポーネントであることをAppComponentに認識させ、そのためのコードを生成できるようにする必要があります。これを行うには、Daggerモジュールを作成する必要があります。
diパッケージにAppSubcomponents.ktというファイルを作成してみましょう。そのファイルでは、@Moduleアノテーションが付けられたAppSubcomponentsというクラスを定義します。サブコンポーネントに関する情報を指定するには、次のように、アノテーションのサブコンポーネント変数にコンポーネントクラス名のリストを追加します。
// This module tells AppComponent which are its subcomponents
@Module(subcomponents = [RegistrationComponent::class])
class AppSubcomponents
この新しいモジュールもAppComponentに含める必要があります。
@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent { ... }
登録固有のビュークラスは、RegistrationComponentによって挿入されます。
RegistrationViewModelとEnterDetailsViewModelはRegistrationComponentを使用するクラスによってのみ要求されるため、それらはRegistrationComponentの一部です。AppComponentの一部ではありません。
11.スコーピングサブコンポーネント
アクティビティとフラグメント間でRegistrationViewModelの同じインスタンスを共有する必要があるため、サブコンポーネントを作成しました。前と同じように、同じスコープアノテーションでコンポーネントとクラスにアノテーションを付けると、その型はコンポーネント内に一意のインスタンスを持つようになります。
ただし、@SingletonはAppComponentで既に使用されているため使用できません。 別のものを作成する必要があります。
この場合、このスコープを*@RegistrationScopeと呼ぶことができますが、これは良い習慣ではありません。スコープアノテーションの名前は、それが果たす目的を明示するものであってはなりません。注釈は兄弟コンポーネント(LoginComponent、SettingsComponentなど)によって再利用できるため、有効期間に応じて名前を付ける必要があります。そのため、@RegistrationScopeと呼ぶ代わりに@ActivityScope*と呼びます。
スコープ規則:
・タイプがスコープアノテーションでマークされている場合、同じスコープでアノテーションが付けられているコンポーネントでのみ使用できます。
・コンポーネントがスコープアノテーションでマークされている場合、そのアノテーションを持つ型またはアノテーションのない型のみを提供できます。サブコンポーネントは、その親コンポーネントの1つが使用するスコープアノテーションを使用できません。コンポーネントには、このコンテキストのサブコンポーネントも含まれます。
diパッケージにActivityScope.ktというファイルを作成し、次のようにActivityScopeの定義を追加しましょう。
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope
RegistrationViewModelをRegistrationComponentにスコープするには、クラスとインターフェイスの両方に*@ActivityScope*の注釈を付ける必要があります。
// ViewModelからコンポーネントへのスコープ*@ActivityScope*を使用する
@ActivityScope
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
...
}
// RegistrationComponentが使用するアノテーションスコープ
// *@ActivityScope*アノテーションが付けられたクラスは、このコンポーネントに一意のインスタンスを持ちます
@ActivityScope
@Subcomponent
interface RegistrationComponent { ... }
RegistrationComponentのインスタンスがRegistrationViewModelのインスタンスを提供するたびに、それは同じものになります。
サブコンポーネントのライフサイクル
アプリケーションがメモリ内にある限り、グラフの同じインスタンスを使用するため、AppComponentはアプリケーションのライフサイクルにアタッチされます。
RegistrationComponentのライフサイクルは何ですか?必要な理由の1つは、RegistrationViewModeの同じインスタンスを登録アクティビティとフラグメント間で共有したかったためです。しかし、また、新しい登録フローがあるたびに、RegistrationViewModelの異なるインスタンスが必要です。
RegistrationActivityは、RegistrationComponentの適切なライフタイムです。新しいアクティビティごとに、RegistrationComponentのインスタンスを使用できる新しいRegistrationComponentとフラグメントを作成します。
RegistrationComponentはRegistrationActivityライフサイクルにアタッチされているため、ApplicationクラスでappComponentへの参照を保持したのと同じ方法で、Activityでコンポーネントへの参照を保持する必要があります。このようにして、フラグメントはそれにアクセスできるようになります。
class RegistrationActivity : AppCompatActivity() {
// RegistrationComponentのインスタンスを保存して、そのフラグメントがアクセスできるようにします
lateinit var registrationComponent: RegistrationComponent
...
}
また、アクティビティをappComponentに注入する代わりに、super.onCreateを呼び出してregistrationComponentを注入する前に、onCreateメソッドでRegistrationComponentの新しいインスタンスを作成する必要があります。
- appComponentからファクトリーを取得し、createを呼び出して、RegistrationComponentの新しいインスタンスを作成します。これは、RegistrationComponentファクトリのインスタンスを返すためにAppComponentインターフェイスで関数registrationComponentを公開するために可能です。
- そのインスタンスをアクティビティのregistrationComponent変数に割り当てます。
- 最近作成されたregistrationComponentにアクティビティを注入して、フィールド注入を実行し、@Injectアノテーションが付けられたフィールドに入力します。
class RegistrationActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
// 行を削除
(application as MyApplication).appComponent.inject(this)
// これらの行を追加
// アプリグラフからファクトリを取得することにより、登録コンポーネントのインスタンスを作成します
// registrationComponent = (application as MyApplication).appComponent.registrationComponent().create()
// このアクティビティを、作成したばかりの登録コンポーネントに挿入します
registrationComponent.inject(this)
super.onCreate(savedInstanceState)
...
}
...
}
気づいたら、変数registrationComponentには@Injectアノテーションが付けられていません。その変数がDaggerによって提供されることを期待していないためです。
registrationComponentはRegistrationActivityで使用でき、そのインスタンスを使用して登録フラグメントを注入できます。アクティビティのregistrationComponentを使用するには、フラグメントのonAttachメソッドを置き換えます。
class EnterDetailsFragment : Fragment() {
...
override fun onAttach(context: Context) {
super.onAttach(context)
(activity as RegistrationActivity).registrationComponent.inject(this)
}
...
}
そして、TermsAndConditionsフラグメントにも同じことを行います。
class TermsAndConditionsFragment : Fragment() {
...
override fun onAttach(context: Context) {
super.onAttach(context)
(activity as RegistrationActivity).registrationComponent.inject(this)
}
}
アプリを再度を実行し、以前のように登録フローに移動して新規に開始すると、登録フローが期待どおりに機能することがわかります。
これは、アプリケーショングラフがどのように見えるかです。
前の図との違いは、RegistrationViewModelがRegistrationComponentにスコープされていることです。RegistrationViewModelにオレンジ色のドットでそれを表します。
注意:構成の変更に耐えるためにコンテナが必要な場合は、UI状態の保存ガイドに従ってください。プロセスの停止を処理するのと同じ方法で処理したい場合があります。そうしないと、ローエンドデバイスでアプリの状態が失われる可能性があります。
12.ログインフローのリファクタリング
オブジェクトを異なるライフサイクルにスコープすることとは別に、サブコンポーネントを作成することは、アプリケーションの異なる部分を互いにカプセル化するための良い習慣です。
アプリの構造に応じてアプリのフローに応じて異なるダガーサブグラフを作成すると、メモリと起動時間の点でパフォーマンスと拡張性に優れたアプリケーションになります。ベストプラクティスの反対は、アプリケーションのすべてのオブジェクトを提供する方法を知っているモノリシックコンポーネントを持つことです。Daggerコンポーネントの読み取りとモジュール化をより困難にします。
ログインフローをリファクタリングして、Daggerを使用してログインフロー用の別のサブコンポーネントを作成しましょう。
ログインパッケージにLoginComponent.ktというファイルを作成し、RegistrationComponentで行ったようにLoginComponentの定義を追加しますが、今回はLogin関連クラスを使用します。
// LoginComponentが使用するスコープアノテーション
// @ActivityScopeアノテーションが付けられたクラスは、このコンポーネントで一意のインスタンスを持ちます
@ActivityScope
// Daggerサブコンポーネントの定義
@Subcomponent
interface LoginComponent {
// LoginComponentのインスタンスを作成するファクトリー
@Subcomponent.Factory
interface Factory {
fun create(): LoginComponent
}
// このコンポーネントによって注入できるクラス
fun inject(activity: LoginActivity)
}
コンポーネントの有効期間はLoginActivityと同じであるため、LoginComponentにActivityScopeでアノテーションを付けることができます。
LoginViewModelのインスタンスを作成する方法をDaggerに伝えるために、コンストラクタに@Injectアノテーションを付けます。
class LoginViewModel @Inject constructor(private val userManager: UserManager) {
...
}
この場合、LoginViewModelを他のクラスで再利用する必要はありません。そのため、@ActivityScopeアノテーションを付けないでください。
また、AppSubcomponentsモジュールのAppComponentのサブコンポーネントのリストに新しいサブコンポーネントを追加する必要があります。
@Module(subcomponents = [RegistrationComponent::class, LoginComponent::class])
class AppSubcomponents
LoginActivityがLoginComponentファクトリにアクセスできるようにするには、AppComponentインターフェースで公開する必要があります。
@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
...
// グラフから取得できる型
fun registrationComponent(): RegistrationComponent.Factory
fun loginComponent(): LoginComponent.Factory
// このコンポーネントによって注入できるクラス
fun inject(activity: MainActivity)
}
LoginComponentのインスタンスを作成し、LoginActivityに注入する準備がすべて整いました。
- Daggerによって提供されるようにするため、loginViewModelフィールドに*@Inject*の注釈を付けます。そしてprivate修飾子を削除します。
- loginComponent()メソッドを呼び出すappComponentからLoginComponentファクトリを取得します。create()でLoginComponentのインスタンスを作成します。アクティビティを渡すComponentのinjectメソッドを呼び出します。
- 以前の手動依存性注入の実装からloginViewModelのインスタンス化を削除します。
class LoginActivity : AppCompatActivity() {
// 1) LoginViewModelはDaggerによって提供されます
@Inject
lateinit var loginViewModel: LoginViewModel
...
override fun onCreate(savedInstanceState: Bundle?) {
// 2) アプリグラフからファクトリを取得して、ログインコンポーネントのインスタンスを作成します
// このアクティビティをそのコンポーネントに注入します
(application as MyApplication).appComponent.loginComponent().create().inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
// 3) インスタンス化を削除
loginViewModel = LoginViewModel((application as MyApplication).userManager)
...
}
}
アプリを再度を実行すると、設定を除くすべてが機能します。これは、設定がDaggerを使用するようにリファクタリングされておらず、UserManagerの別のインスタンスを使用しているためです。
新しいログインコンポーネントでは、アプリケーショングラフは次のようになります。
この時点でのコードラボのソリューションは、Githubプロジェクトの2_subcomponentsブランチにあります。
13.同じスコープを持つ複数のアクティビティ
Daggerを使用するように設定をリファクタリングしましょう。SettingsActivityフィールドをDaggerによって挿入する必要があるため:
1.Daggerに、コンストラクターに@Injectアノテーションを付けて、SettingsActivity依存関係(つまり、SettingsViewModel)のインスタンスを作成する方法を伝えます。Daggerは、SettingsViewModel依存関係のインスタンスを作成する方法をすでに知っています。
class SettingsViewModel @Inject constructor(
private val userDataRepository: UserDataRepository,
private val userManager: UserManager
) { ... }
2.AppComponentインターフェースのSettingsActivityをパラメーターとして使用する関数を追加することにより、SettingsActivityがDaggerによって注入されることを許可します。
@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
...
fun inject(activity: SettingsActivity)
}
3.SettingsActivityで、挿入されたフィールドに*@Injectのアノテーションを付け、private修飾子を削除します。
4.MyApplicationからappComponentにアクセスするアクティビティを挿入して、inject(this)を呼び出して、@Injectアノテーションが付けられたフィールドに入力します。
5.手動の依存関係注入の古い実装に必要なインスタンス化を削除します。
class SettingsActivity : AppCompatActivity() {
// 1) SettingsViewModelはDaggerによって提供されます
@Inject
lateinit var settingsViewModel: SettingsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
// 2) appComponentを注入します
(application as MyApplication).appComponent.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
// 3) 次の行を削除
val userManager = (application as MyApplication).userManager
settingsViewModel = SettingsViewModel(userManager.userDataRepository!!, userManager)
...
}
}
アプリを実行すると、[設定]の[通知の更新]機能が機能しないことがわかります。 これは、MainActivityとSettingsActivityでUserDataRepositoryの同じインスタンスを再利用していないためです!
@Singletonアノテーションを付けることで、UserDataRepositoryをAppComponentにスコープできますか?前と同じ推論に従って、ユーザーがログアウトまたは登録解除した場合、UserDataRepositoryの同じインスタンスをメモリに保持したくないため、これを行いたくありません。そのデータは、ログインしているユーザーに固有です。
ユーザーがログインしている限り有効なコンポーネントを作成します。ユーザーがログインした後にアクセスできるすべてのアクティビティは、このコンポーネント(つまり、MainActivityとSettingsActivity)によって注入されます
LoginComponentとRegistrationComponentで行ったようにUserComponentを呼び出すことができる別のサブコンポーネントを作成しましょう。
1.ユーザーフォルダーにUserComponent.ktというKotlinファイルを作成します。
2.@Subcomponentアノテーションが付けられたUserComponentという名前のインターフェイスを作成します。このインターフェイスは、ユーザーがログインしてファクトリーを作成した後に発生するクラスを挿入できます。
// Daggerサブコンポーネントの定義
@Subcomponent
interface UserComponent {
// UserComponentのインスタンスを作成するファクトリ
@Subcomponent.Factory
interface Factory {
fun create(): UserComponent
}
// このコンポーネントによって注入できるクラス
fun inject(activity: MainActivity)
fun inject(activity: SettingsActivity)
}
3.この新しいサブコンポーネントを、AppSubcomponents.ktファイル内のAppComponentのサブコンポーネントのリストに追加します。
@Module(subcomponents = [RegistrationComponent::class, LoginComponent::class, UserComponent::class])
class AppSubcomponents
UserComponentのライフタイムを担当しているのは何ですか?LoginComponentとRegistrationComponentはそのアクティビティによって管理されますが、UserComponentは複数のアクティビティを挿入でき、アクティビティの数が増加する可能性があります。
このコンポーネントの有効期間を、ユーザーがいつログインおよびログアウトしたかを知っているものにアタッチする必要があります。この場合、UserManagerが実行します。 UserManagerは、登録、ログイン、およびログアウトの試行を処理します。UserComponentインスタンスが存在することは理にかなっています。
UserManagerがUserComponentの新しいインスタンスを作成する必要がある場合、UserComponentファクトリにアクセスする必要があります。ファクトリをコンストラクタパラメータとして追加すると、UserManagerのインスタンスを作成するときにDaggerが提供します。
@Singleton
class UserManager @Inject constructor(
private val storage: Storage,
// UserManagerはUserComponentライフサイクルの管理を担当するため、
// インスタンスの作成方法を知る必要があります
private val userComponentFactory: UserComponent.Factory
) {
...
}
手動の依存性注入では、セッションのユーザーデータをUserManagerに保存しました。これで、ユーザーがログインしているかどうかが決まりました。代わりにUserComponentでも同じことができます。
UserManagerにUserComponentのインスタンスを保持して、その有効期間を管理できます。UserComponentがnullでない場合、ユーザーはログインします。 ユーザーがログアウトすると、UserComponentのインスタンスを削除できます。このように、UserComponentには特定のユーザーに関連するクラスのすべてのデータとインスタンスが含まれているため、ユーザーがログアウトすると、コンポーネントを破棄すると、すべてのデータがメモリから削除されます。
UserDataRepositoryの代わりにUserComponentのインスタンスを使用するようにUserManagerを変更します。
@Singleton
class UserManager @Inject constructor(...) {
// 行を削除
var userDataRepository: UserDataRepository? = null
var userComponent: UserComponent? = null
private set
fun isUserLoggedIn() = userComponent != null
fun logout() {
userComponent = null
}
private fun userJustLoggedIn() {
userComponent = userComponentFactory.create()
}
}
上記のコードでわかるように、ユーザーがUserComponentファクトリのcreateメソッドを使用してログインすると、userComponentのインスタンスが作成されます。そして、logout()が呼び出されたときにインスタンスを削除します。
MainActivityとSettingsActivityの両方が同じインスタンスを共有できるように、UserDataRepositoryをUserComponentにスコープする必要があります。
有効期間を管理するアクティビティを持つコンポーネントに注釈を付けるためにスコープアノテーション*@ActivityScopeを使用しているため、すべてのアプリケーションではなく、複数のアクティビティをカバーできるスコープが必要です。そのようなものはまだないので、新しいスコープを作成する必要があります。
このスコープは、ユーザーがログインしたときのライフタイムをカバーするため、LoggedUserScopeと呼ぶことができます。
ユーザーパッケージにLoggedUserScope.kt*という新しいKotlinファイルを作成し、LoggedUserScopeスコープアノテーションを次のように定義します。
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class LoggedUserScope
UserComponentが常にUserDataRepositoryの同じインスタンスを提供できるように、UserComponentとUserDataRepositoryの両方にこの注釈を付けることができます。
// UserComponentが使用するスコープ注釈
// *@LoggedUserScope*アノテーションが付けられたクラスには、このコンポーネントに一意のインスタンスがあります
@LoggedUserScope
@Subcomponent
interface UserComponent { ... }
// このオブジェクトには、@ LoggedUserScopeの注釈が付けられたコンポーネントに
// 一意のインスタンスがあります(この場合はUserComponentのみ)。
@LoggedUserScope
class UserDataRepository @Inject constructor(private val userManager: UserManager) {
...
}
MyApplicationクラスには、手動の依存性注入の実装に必要なuserManagerのインスタンスを保存しました。このアプリはDaggerを使用するために完全にリファクタリングされているため、もう必要ありません。MyApplicationは次のようになります。
open class MyApplication : Application() {
val appComponent: AppComponent by lazy {
DaggerAppComponent.factory().create(applicationContext)
}
}
AppComponentも変更する必要があります。
- MainActivityとSettingsActivityはこのコンポーネントによってもう注入されないので、注入メソッドを削除します。これらはUserComponentを使用します。
- MainActivityおよびSettingsActivityがUserComponentのインスタンスにアクセスするために必要とするため、グラフからUserManagerを公開します。
@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
...
// 2) MainActivityとSettingsActivityがUserComponentの特定のインスタンスに
// アクセスできるように、UserManagerを公開します
fun userManager(): UserManager
// 1) 次の行を削除
fun inject(activity: MainActivity)
fun inject(activity: SettingsActivity)
}
SettingsActivityで、@Injectを使用してViewModelに注釈を付け(Daggerによって注入されるようにするため)、プライベート修飾子を削除します。ユーザーがログインしているために初期化されるUserComponentのインスタンスを取得するには、appComponentが公開するuserManager()メソッドを呼び出します。これで、内部のuserComponentにアクセスして、アクティビティを注入できます。
class SettingsActivity : AppCompatActivity() {
// @Inject注釈付きフィールドはDaggerによって提供されます
@Inject
lateinit var settingsViewModel: SettingsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
// アプリケーショングラフからuserManagerを取得してUserComponentのインスタンスを取得し、
// このアクティビティを挿入します
val userManager = (application as MyApplication).appComponent.userManager()
userManager.userComponent!!.inject(this)
super.onCreate(savedInstanceState)
...
}
...
}
MainActivityは、UserComponentを注入するために同じことを行います。
- UserManagerはappComponentから直接取得できるため、もう注入しないでください。 userManagerフィールドを削除する
- ユーザーがログインしているかどうかを確認する前に、ローカル変数を作成します。
- UserComponentはユーザーがログインしている場合にのみ利用できるため、userManagerからuserComponentを取得し、elseブランチにアクティビティを注入します。
class MainActivity : AppCompatActivity() {
// 1) userManagerフィールドを削除
@Inject
lateinit var userManager: UserManager
@Inject
lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
// 2) appComponentからuserManagerを取得して、ユーザーがログインしているかどうかを確認します
val userManager = (application as MyApplication).appComponent.userManager()
if (!userManager.isUserLoggedIn()) { ... }
else {
setContentView(R.layout.activity_main)
// 3) MainActivityを表示する必要がある場合、
// アプリケーショングラフからUserComponentを取得し、このアクティビティを挿入します
userManager.userComponent!!.inject(this)
setupViews()
}
}
...
}
重要:条件付きフィールドインジェクション(ユーザーがログインしている場合にのみインジェクトするときMainActivity.ktで行うように)を行うことは非常に危険です。開発者は条件に注意する必要があり、注入されたフィールドとやり取りするときにNullPointerExceptionsを取得するリスクがあります。
この問題を回避するために、ユーザーの状態に応じて登録、ログイン、またはメインのいずれかにルーティングするSplashScreenを作成することにより、間接性を追加できます。これは、作業割り当てに関するコードラボの最後のセクションのトピックになります。
アプリのすべての画面は、Daggerにリファクタリングされました!アプリを実行すると、すべてが期待どおりに動作するようになりました。
UserComponentを追加すると、アプリケーショングラフは次のようになります。
この時点でのコードラボのソリューションは、Githubプロジェクトの3_dagger_appブランチにあります。
14. Daggerを使用したテスト
Daggerのような依存性注入フレームワークを使用する利点の1つは、コードのテストが簡単になることです。
Unit tests
単体テストにDagger関連のコードを使用する必要はありません。コンストラクター注入を使用するクラスをテストする場合、そのクラスをインスタンス化するためにDaggerを使用する必要はありません。フェイクの依存関係またはモックの依存関係を渡すコンストラクタを直接呼び出すことができます。
たとえば、LoginViewModelをテストするLoginViewModelTest.ktファイルを見ると、UserManagerをモックして、Daggerを使用しない場合と同様にパラメーターとして渡すだけです。
class LoginViewModelTest {
...
private lateinit var viewModel: LoginViewModel
private lateinit var userManager: UserManager
@Before
fun setup() {
userManager = mock(UserManager::class.java)
viewModel = LoginViewModel(userManager)
}
@Test
fun `Get username`() {
whenever(userManager.username).thenReturn("Username")
val username = viewModel.getUsername()
assertEquals("Username", username)
}
...
}
すべての単体テストは、1つを除き、手動の依存関係注入と同じままです。UserComponent.FactoryをUserManagerに追加したときに、ユニットテストを中断しました。ファクトリでcreate()を呼び出すときにDaggerが返すものをモックする必要があります。
もしかして: Open the UserManager Test.kt file and create and configure mocks for the User Component factory as follows:
UserManagerTest.ktファイルを開き、次のようにUserComponentファクトリのモックを作成および構成します。
class UserManagerTest {
...
@Before
fun setup() {
// ファクトリを呼び出すときに、模擬*userComponent*を返します
val userComponentFactory = Mockito.mock(UserComponent.Factory::class.java)
val userComponent = Mockito.mock(UserComponent::class.java)
`when`(userComponentFactory.create()).thenReturn(userComponent)
storage = FakeStorage()
userManager = UserManager(storage, userComponentFactory)
}
...
}
これで、すべての単体テストに合格するはずです。
エンドツーエンドのテスト
Daggerなしで統合テストを実行しました。プロジェクトにDaggerを導入し、MyApplicationクラスの実装を変更するとすぐに、それらを壊しました。
インストルメンテーションテストでのカスタムアプリケーションの使用
以前は、エンドツーエンドテストではMyTestApplicationというカスタムアプリケーションを使用していました。別のアプリケーションを使用するには、新しいTestRunnerを作成する必要がありました。
そのためのコードは、app/src/androidTest/java/com/example/android/dagger/MyCustomTestRunner.ktファイルにあります。コードはすでにプロジェクトにあります。追加する必要はありません。
class MyCustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, MyTestApplication::class.java.name, context)
}
}
プロジェクトは、このTestRunnerがapp/build.gradleファイルで指定されているため、インストルメンテーションテストを実行するときに使用する必要があることを知っています。
...
android {
...
defaultConfig {
...
testInstrumentationRunner "com.example.android.dagger.MyCustomTestRunner"
}
...
}
...
計装テストでのDaggerの使用
Daggerを使用するには、MyTestApplicationを構成する必要があります。 統合テストの場合、テスト用のTestApplicationComponentを作成することをお勧めします。生産とテストは異なるコンポーネント構成を使用します。
テスト構成と実稼働構成の違いは何ですか?UserManagerでSharedPreferencesStorageを使用する代わりに、FakeStorageを使用します。SharedPreferencesStorageを生成しているのは何ですか? StorageModule。
FakeStorageを使用する別のStorageModuleにStorageModuleを交換する必要があります。
これはインストルメンテーションテストにのみ必要なため、この新しいクラスをandroidTestフォルダーに作成します。
app/src/androidTest/java/com/example/android/dagger/内にdiという新しいパッケージを作成します。
そこで、パスがapp/src/androidTest/java/com/example/android/dagger/di/TestStorageModule.ktであるTestStorageModule.ktという新しいファイルを作成します。
// AndroidテストでStorageModuleをオーバーライドします
@Module
abstract class TestStorageModule {
// ストレージタイプが要求されたときにDaggerがFakeStorageを提供するようにします
@Binds
abstract fun provideStorage(storage: FakeStorage): Storage
}
@Bindsの仕組みにより、SharedPreferencesStorageをパラメーターとして使用してメソッドを宣言する代わりに、
TestStorageModuleの場合、FakeStorageをパラメーターとして渡します。これにより、次に作成するTestAppComponentがストレージのこの実装を使用するようになります。
DaggerはFakeStorageのインスタンスを作成する方法を知りませんが、いつものように、コンストラクタに*@Inject*アノテーションを付けます。
class FakeStorage @Inject constructor(): Storage { ... }
ここで、Daggerがストレージタイプを要求したときにFakeStorageのインスタンスを提供します。生産とテストでは異なるコンポーネント構成を使用するため、AppComponentとして機能する別のコンポーネントを作成する必要があります。TestAppComponentと呼びます。
次のパスに新しいKotlinファイルを作成しましょう
app/src/androidTest/java/com/example/android/dagger/di/TestAppComponent.kt
@Singleton
@Component(modules = [TestStorageModule::class, , AppSubcomponents::class])
interface TestAppComponent : AppComponent
このテストコンポーネントのすべてのモジュールも指定する必要があります。TestStorageModuleとは別に、サブコンポーネントに関する情報を追加するAppSubcomponentsモジュールも含める必要があります。テストグラフにはコンテキストが必要ないため(以前にコンテキストが必要だった唯一の依存関係はSharedPreferencesStorageでした)、TestAppComponentのファクトリを定義する必要はありません。
アプリをビルドしようとすると、DaggerはTestAppComponentの実装を生成しないことがわかります。テストグラフを使用してDaggerTestAppComponentクラスを作成する必要があります。kaptがandroidTestフォルダーで動作していないためです。次のように、DaggerアノテーションプロセッサアーティファクトをandroidTestに追加する必要があります。
...
dependencies {
...
kaptAndroidTest "com.google.dagger:dagger-compiler:$dagger_version"
}
プロジェクトを同期してアプリをビルドすると、DaggerTestAppComponentが使用可能になります。そうでない場合は、androidTestフォルダーでまだ動作していないためです。androidTestフォルダー内のjavaフォルダーを右クリックしてインストルメンテーションテストを実行し、「すべてのテストを実行」をクリックします。
MyTestApplicationが独自のDaggerコンポーネントを作成できるようにするには、MyApplicationにいくつかの変更を加える必要があります。
by lazy bodyレ内によるappComponentの初期化を別のメソッドに抽出します。initializeComponents()というMyTestComponentでオーバーライドして、開くことができます。
open class MyApplication : Application() {
val appComponent: AppComponent by lazy {
initializeComponent()
}
open fun initializeComponent(): AppComponent {
return DaggerAppComponent.factory().create(applicationContext)
}
}
これで、My Applicationをサブクラス化し、MyTest ApplicationでTest AppComponentを使用できます。
- userManagerインスタンスを削除します
- initializeComponentメソッドをオーバーライドして、DaggerTestAppComponentのインスタンスを返すようにします。
class MyTestApplication : MyApplication() {
override fun initializeComponent(): AppComponent {
// フェイクタイプを注入する新しいTestAppComponentを作成します
return DaggerTestAppComponent.create()
}
}
注意:DaggerTestAppComponentをインポートできない場合は、以下に指定されているインストルメンテーションテストを実行してから、インポートを試行してください。プロジェクトはandroidTest構成にないため、そのクラスは生成されません。
これでテストに合格するはずです。androidTest/java/com/example/android/daggerフォルダーのApplicationTest.kt*ファイルを開き、クラス定義の横にある実行ボタンをクリックします。テストが実行されてパスするはずです。
この時点でのコードラボのソリューションは、Githubプロジェクトのsolutionブランチにあります。
15. @Provides注釈と修飾子
Androidプロジェクトで役立つ他のアノテーションがあります。
@Provides
@Injectおよび*@Bindsアノテーションとは別に、@Providesを使用して、DaggerにDaggerモジュール内のクラスのインスタンスを提供する方法を指示できます。
@Provides関数の戻り値の型(関数の呼び出し方法は関係ありません)は、Daggerのグラフに追加される型を伝えます。その関数のパラメーターは、その型のインスタンスを提供する前にDaggerが満たす必要がある依存関係です。
この例では、次のようにStorage*タイプの実装も提供できます。
@Module
class StorageModule {
// @Providesは、この関数が返す型(つまり、ストレージ)
// のインスタンスを作成する方法をDaggerに伝えます。
// 関数パラメーターは、このタイプの依存関係(つまり、コンテキスト)です。
@Provides
fun provideStorage(context: Context): Storage {
// DaggerがStorageタイプのインスタンスを提供する必要があるときはいつでも、
// このコード(@Providesメソッド内のコード)が実行されます。
return SharedPreferencesStorage(context)
}
}
Daggerモジュールで*@Provides*アノテーションを使用して、Daggerに提供方法を伝えることができます。
- インターフェースの実装(ただし、@Bindsは、生成されるコードが少なく、より効率的であるため推奨されます)
- プロジェクトが所有していないクラス(Retrofitのインスタンスなど)
修飾子
プロジェクトのDagger修飾子は単純であるため、使用する必要はありませんでした。修飾子は、同じタイプの異なる実装をDaggerグラフに追加する場合に役立ちます。たとえば、異なるStorageオブジェクトを提供したい場合、修飾子を使用してそれらを区別できます。
修飾子は、依存関係を識別するために使用されるカスタムアノテーションです。
たとえば、ファイル名をパラメータとして使用するSharedPreferencesStorageがある場合
class SharedPreferencesStorage @Inject constructor(name: String, context: Context) : Storage {
private val sharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE)
...
}
StorageModuleの*@Provides*を使用して、さまざまな実装を追加できます。修飾子を使用して、実装の種類を識別できます。
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class RegistrationStorage
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class LoginStorage
@Module
class StorageModule {
@RegistrationStorage
@Provides
fun provideRegistrationStorage(context: Context): Storage {
return SharedPreferencesStorage("registration", context)
}
@LoginStorage
@Provides
fun provideLoginStorage(context: Context): Storage {
return SharedPreferencesStorage("login", context)
}
}
この例では、@Providesメソッドにアノテーションを付けるために使用できるRegistrationStorageとLoginStorageの2つの修飾子を定義しました。グラフには、RegistrationStorageとLoginStorageの2種類のストレージを追加しています。どちらのメソッドもStorageを返し、同じパラメーター(依存関係)を持ちますが、名前は異なります。@Provides関数の名前には機能がないため、次のように修飾子を使用してグラフから取得する必要があります。
修飾子を依存関係として取得する方法の例
// In a method
class ClassDependingOnStorage(@RegistrationStorage private val storage: Storage) { ... }
// As an injected field
class ClassDependingOnStorage {
@Inject
@field:RegistrationStorage lateinit var storage: Storage
}
@Namedアノテーションを使用して同じ修飾子の機能を実現できますが、次の理由で修飾子が推奨されます。
- ProguardまたはR8から削除できます
- 名前を一致させるために共有定数を保持する必要はありません
- それらは文書化することができます
16. [オプション]独自に依存性注入を試してください
コードラボアプリには、もう1つの試してみるための部分があります。それは、SplashScreenを使用するためのリファクタリングです。MainActivity.ktは、アプリを開くときに表示する必要がある画面のロジックを処理します。ユーザーがMainActivityにとどまるときにのみ注入する条件付き依存性注入を行っているため、これは問題です。
これらの手順にはコメントやコードが含まれていないため、自分で試してください。
- 表示する画面のロジックを処理するSplashViewModelでSplashActivityを作成します。
- これまでやってきたように、SplashActivityで依存性注入を使用して、Daggerによって注入されたフィールドを取得します。
- アクティビティが開かれるとユーザーがログインするため、MainActivity.ktのonCreateメソッドのロジックを削除します。
17.おめでとうございます!
これでDaggerに慣れてきたので、DaggerをAndroidアプリに追加できるはずです。 このコードラボでは、次のことを学びました。
- Daggerの*@Component*アノテーションを使用してアプリケーショングラフを作成する方法
- @Inject、@Module、@Bindsおよび*@BindsInstance*アノテーションを使用して、グラフに情報を追加する方法
- @Subcomponentを使用してフローコンテナーを作成する方法
- スコープを使用して、異なるコンテナ内のオブジェクトのインスタンスを再利用する方法
- ダガー修飾子と*@Provides*アノテーション
- Daggerを使用するアプリケーションを単体テストとインストゥルメント化テストでテストする方法