この Codelab では、信頼性が高く、大規模なプロジェクトに対して拡張可能なアプリを作成するための依存関係注入(DI)の重要性を学びます。依存関係の管理には、DI ツールとして Hilt を使用します。
依存関係注入はプログラミングで広く使用されている手法で、Android 開発にも適しています。DI の原則に従うことで、優れたアプリ アーキテクチャの土台を築くことができます。
依存関係注入を実装すると、次のようなメリットがもたらされます。
- コードを再利用できる
- リファクタリングが容易になる
- テストが容易になる
Hilt は Android 用の独自の依存性注入ライブラリです。これを使うことで、プロジェクトで DI を手動で行うためのボイラープレートが減ります。手動で依存関係注入を行うには、各クラスとその依存関係を手作業で作成し、コンテナを使用して依存関係の再利用と管理を行う必要があります。
Hilt は、プロジェクトのすべての Android コンポーネントにコンテナを提供し、コンテナのライフサイクルを自動的に管理することで、アプリで DI 注入を行うための標準的な方法を定義します。これを実現するには、一般的な DI ライブラリ Dagger を利用します。
この Codelab で問題(コードのバグ、文法的な誤り、不明確な表現など)が見つかった場合は、Codelab の左下隅にある [誤りを報告] から問題を報告してください。
前提条件
- Kotlin 構文を使用した経験がある。
- アプリで依存関係注入が重要な理由を理解している。
学習内容
- Android アプリで Hilt を使用する方法。
- 持続可能なアプリの作成に関連する Hilt の概念。
- 修飾子を使用して、同じ型に複数のバインディングを追加する方法。
@EntryPoint
を使用して、Hilt でサポートされていないクラスからコンテナにアクセスする方法。- 単体テストとインストルメンテーション テストを通じて、Hilt を使用するアプリをテストする方法。
必要なもの
- Android Studio 4.0 以降。
コードの取得
次のコマンドで、GitHub から Codelab のコードを取得します。
$ git clone https://github.com/googlecodelabs/android-hilt
または、リポジトリを ZIP ファイルとしてダウンロードすることもできます。
Android Studio を開く
この Codelab には Android Studio 4.0 以降が必要です。Android Studio をダウンロードする必要がある場合は、こちらからダウンロードします。
サンプルアプリの実行
この Codelab では、アプリに Hilt を追加します。このアプリは、ユーザーの操作を記録し、Room を使用してローカル データベースにデータを保存します。
次の手順に沿って、Android Studio でサンプルアプリを開いてください。
- zip アーカイブをダウンロードした場合は、ファイルをローカルに展開します。
- Android Studio でプロジェクトを開きます。
- [Run] ボタンをクリックして、エミュレータを選択するか、Android デバイスを接続します。
ご覧のように、番号が付けられたボタンを操作するたびにログが作成されて保存されます。[See All Logs] 画面に、以前のすべての操作のリストが表示されます。ログを削除するには、[Delete Logs] ボタンをタップします。
プロジェクトの設定
このプロジェクトには複数の GitHub ブランチがあります。
master
は、チェックアウトまたはダウンロードしたブランチであり、Codelab の出発点です。solution
には、この Codelab の解答があります。
master
ブランチのコードから始め、マイペースで Codelab を進めることをおすすめします。
Codelab の途中には、プロジェクトに追加する必要があるコード スニペットを記載しています。場所によってはコードを削除する必要もありますが、この部分はコード スニペットのコメントに明示的に記載されています。
git を使用して solution
ブランチを取得するには、次のコマンドを使用します。
$ git clone -b solution https://github.com/googlecodelabs/android-hilt
または、次の場所から解答コードをダウンロードします。
よくある質問
Hilt を使う理由
開始用コードを見ると、ServiceLocator
クラスのインスタンスが LogApplication
クラスに格納されていることがわかります。ServiceLocator
は、それを必要とするクラスによってオンデマンドで取得される依存関係を作成して格納します。このクラスは、依存関係のコンテナと考えることができます。このコンテナは、アプリの破棄に伴って破棄されるため、アプリのライフサイクルにアタッチされます。
Android DI に関するガイダンスで説明されているように、サービス ロケータは、最初は比較的少量のボイラープレート コードから始められますが、スケーリングへの対応力は不十分です。大規模な Android アプリを開発するには、Hilt を使用する必要があります。
Hilt では、手動で作成する必要のあったコード(ServiceLocator
クラスのコードなど)が生成され、Android アプリで手動による DI またはサービス ロケータ パターンを使用する際に必要なボイラープレートが不要になります。
次の手順では、Hilt を使用して ServiceLocator
クラスを置き換えます。その後、プロジェクトにさらに新しい機能を追加し、他の Hilt 機能を調べます。
プロジェクトでの Hilt
Hilt はすでに master
ブランチ(ダウンロードしたコード)で設定されています。以下に示すコードはすでに実行されているため、プロジェクトに追加する必要はありませんが、Android アプリで Hilt を使用する場合に必要なものを確認する意味で紹介します。
Hilt では、ライブラリの依存関係とは別に、プロジェクトに設定されている Gradle プラグインを使用します。ルートの build.gradle
ファイルを開き、クラスパスで次の Hilt の依存関係を参照します。
buildscript {
...
ext.hilt_version = '2.28-alpha'
dependencies {
...
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
次に、app
モジュールで gradle プラグインを使用するには、app/build.gradle
ファイル内で、このプラグインをファイル上部の kotlin-kapt
プラグインの下に追加して指定します。
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
}
最後に、Hilt の依存関係は、次のように、同じ app/build.gradle
ファイル内のプロジェクトに含まれています。
...
dependencies {
...
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}
すべてのライブラリは、Hilt を含めて、プロジェクトのビルドと同期の際にダウンロードされます。では、Hilt を使ってみましょう。
LogApplication
クラスで使用および初期化されている ServiceLocator
のインスタンスの場合と同じように、アプリのライフサイクルにアタッチされたコンテナを追加するには、Application
クラスに @HiltAndroidApp
アノテーションを付ける必要があります。LogApplication.kt
を開き、アノテーションをクラスに追加してください。
@HiltAndroidApp
class LogApplication : Application() {
...
}
@HiltAndroidApp
は、Hilt のコード生成をトリガーします。これには、依存関係注入を使用できるアプリの基本クラスも含まれます。アプリケーション コンテナはアプリの親コンテナであることから、他のコンテナは、このコンテナの提供する依存関係にアクセスできます。
これで、アプリで Hilt を使用できるようになりました。
クラスで ServiceLocator
からオンデマンドで依存関係を取得する代わりに、これらの依存関係を提供する Hilt を使用します。まず、クラスからの ServiceLocator
の呼び出しを置き換えます。
ui/LogsFragment.kt
ファイルを開きます。LogsFragment
は onAttach
でフィールドの入力を行います。ServiceLocator
を使用して LoggerLocalDataSource
と DateFormatter
のインスタンスを手動で入力する代わりに、Hilt を使用して、これらの型のインスタンスを作成し、管理することができます。
LogsFragment
で Hilt を使用するには、@AndroidEntryPoint
アノテーションを付けます。
@AndroidEntryPoint
class LogsFragment : Fragment() {
...
}
Android クラスに @AndroidEntryPoint
アノテーションを付けると、Android クラスのライフサイクルに沿った依存関係コンテナが作成されます。
@AndroidEntryPoint
を使用すると、Hilt は、LogsFragment
のライフサイクルにアタッチされる依存関係コンテナを作成し、インスタンスを LogsFragment
に注入できるようになります。Hilt で注入するフィールドはどのように指定するのでしょうか。
注入したいフィールド(logger
、dateFormatter
)に @Inject
アノテーションを付けることで、Hilt にさまざまな型のインスタンスを注入させることができます。
@AndroidEntryPoint
class LogsFragment : Fragment() {
@Inject lateinit var logger: LoggerLocalDataSource
@Inject lateinit var dateFormatter: DateFormatter
...
}
これがいわゆるフィールド注入です。
Hilt ではこれらのフィールドが自動的に入力されるため、populateFields
メソッドは不要になりました。次のように、クラスからこのメソッドを削除しましょう。
@AndroidEntryPoint
class LogsFragment : Fragment() {
// Remove following code from LogsFragment
override fun onAttach(context: Context) {
super.onAttach(context)
populateFields(context)
}
private fun populateFields(context: Context) {
logger = (context.applicationContext as LogApplication).serviceLocator.loggerLocalDataSource
dateFormatter =
(context.applicationContext as LogApplication).serviceLocator.provideDateFormatter()
}
...
}
内部では、Hilt は自動生成した LogsFragment
の依存関係コンテナで作成されたインスタンスを onAttach()
ライフサイクル メソッドでこれらのフィールドに入力します。
フィールド注入を実行するには、これらの依存関係のインスタンスを提供する方法を Hilt が把握している必要があります。この例では、Hilt は LoggerLocalDataSource
と DateFormatter
のインスタンスを提供する方法を把握する必要があります。しかし、これらのインスタンスを提供する方法は Hilt にはまだわかりません。
@Inject を使って依存関係の提供方法を Hilt に指示する
ServiceLocator.kt
ファイルを開き、ServiceLocator
の実装をご覧ください。provideDateFormatter()
の呼び出しにより、常に DateFormatter
の異なるインスタンスが返されているのがわかります。
これは、Hilt で行う動作とまったく同じ動作です。DateFormatter
は他のクラスに依存しないため、現時点では推移的な依存関係について心配する必要はありません。
特定の型のインスタンスを提供する方法を Hint に指示するには、注入させるクラスのコンストラクタに @Inject アノテーションを追加します。
util/DateFormatter.kt
ファイルを開き、DateFormatter
のコンストラクタに @Inject
アノテーションを付けます。Kotlin でコンストラクタにアノテーションを付けるには、次のように、constructor
キーワードも必要になります。
class DateFormatter @Inject constructor() { ... }
これにより、Hilt は DateFormatter
のインスタンスを提供する方法を把握します。LoggerLocalDataSource
についても同様に処理します。data/LoggerLocalDataSource.kt
ファイルを開き、そのコンストラクタに @Inject
アノテーションを付けます。
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
...
}
ServiceLocator
クラスをもう一度開くと、パブリック LoggerLocalDataSource
フィールドがあることがわかります。つまり、ServiceLocator
が呼び出されるたびに常に LoggerLocalDataSource
の同じインスタンスが返されます。これは「インスタンスのスコープをコンテナに設定する」と呼ばれます。Hilt ではこれをどのように実現しているのでしょうか。
アノテーションを使用して、インスタンスのスコープをコンテナに設定できます。Hilt ではライフサイクルごとに異なるコンテナが生成されるため、スコープをこれらのコンテナに設定するためにさまざまなアノテーションを使用できます。
インスタンスのスコープをアプリケーション コンテナに設定するアノテーションは @Singleton
です。このアノテーションを使用すれば、その型が別の型の依存関係として使用されているかどうか、フィールド注入が必要かどうかにかかわらず、アプリケーション コンテナは常に同じインスタンスを提供します。
Android クラスにアタッチされたすべてのコンテナに同じロジックを適用できます。スコープ設定されるすべてのアノテーションのリストについては、こちらのドキュメントをご覧ください。たとえば、アクティビティ コンテナが常に型の同じインスタンスを提供するようにしたい場合は、その型に @ActivityScoped
アノテーションを付けます。
上記のようにアプリケーション コンテナが常に同じ LoggerLocalDataSource
のインスタンスを提供するようにしたいので、そのクラスに @Singleton
アノテーションを付けます。
@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
...
}
これで Hilt は、LoggerLocalDataSource
のインスタンスを提供する方法を把握します。ただし、今回の型には推移的な依存関係があります。LoggerLocalDataSource
のインスタンスを提供するには、Hilt は LogDao
のインスタンスを提供する方法も把握している必要があります。
ただし、LogDao
はインターフェースであり、インターフェースにはコンストラクタがないため、@Inject
アノテーションをコンストラクタに付けることはできません。このようなインスタンスについては、どのように Hilt に指示すればよいでしょうか。
モジュールは、Hilt にバインディングを追加するために使用されます。つまり、さまざまな型のインスタンスを提供する方法を Hillt に指示するために使用されます。Hilt モジュールには、コンストラクタで注入できない型(プロジェクトに含まれていないインターフェースやクラスなど)にバインディングを追加できます。たとえば OkHttpClient
は、そのビルダーを使用してインスタンスを作成する必要があります。
Hilt モジュールは、@Module
と @InstallIn
のアノテーションが付けられたクラスです。@Module
はこれがモジュールであることを Hilt に指示し、@InstallIn
は Hilt コンポーネントを指定することでバインディングを使用できるコンテナを Hilt に指示します。Hilt コンポーネントはコンテナと考えることができます。コンポーネントの完全なリストについては、こちらをご覧ください。
Hilt で注入できる Android クラスごとに、関連付けられた Hilt コンポーネントがあります。たとえば、Application
コンテナは ApplicationComponent
に関連付けられ、Fragment
コンテナは FragmentComponent
に関連付けられます。
モジュールの作成
Hilt モジュールを作成しましょう。ここではバインディングを追加できます。hilt
パッケージの下に di
という新しいパッケージを作成し、パッケージ内に DatabaseModule.kt
という新しいファイルを作成します。
LoggerLocalDataSource
のスコープがアプリケーション コンテナに設定されているため、アプリケーション コンテナで LogDao
バインディングを利用できるようにする必要があります。この要件は、@InstallIn
アノテーションを使用して、このアノテーションに関連付けられている Hilt コンポーネントのクラス(ApplicationComponent:class
)を渡すことで指定します。
package com.example.android.hilt.di
@InstallIn(ApplicationComponent::class)
@Module
object DatabaseModule {
}
ServiceLocator
クラスの実装では、logsDatabase.logDao()
を呼び出すことで LogDao
のインスタンスを取得します。そのため、LogDao のインスタンスを提供するために、AppDatabase
クラスに対する推移的な依存関係を作成しました。
@Provide を使用したインスタンスの提供
Hilt モジュールの関数に @Provides
アノテーションを付けると、コンストラクタで注入できない型を提供する方法を Hilt に指示できます。
@Provides
でアノテーションが付けられた関数の本体は、Hilt がその型のインスタンスを提供する必要があるたびに実行されます。@Provides
アノテーションが付けられた関数の戻り値の型は、バインディングの型、つまりその型のインスタンスを提供する方法を Hilt に指示します。関数パラメータはその型の依存関係です。
この例では、DatabaseModule
クラスにこの関数を追加します。
@Module
object DatabaseModule {
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
}
上記のコードでは、LogDao
のインスタンスを提供する際に、database.logDao()
を実行する必要があることを Hilt に指示しています。推移的な依存関係として AppDatabase
があるため、その型のインスタンスを提供する方法を Hilt に指示する必要があります。
AppDatabase
は、Room によって生成されるため、プロジェクトにない別のクラスです。したがって、データベース インスタンスを ServiceLocator
クラスで構築するのと同様に、@Provides
関数を使用してこのクラスを提供することもできます。
@Module
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
return Room.databaseBuilder(
appContext,
AppDatabase::class.java,
"logging.db"
).build()
}
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
}
常に Hilt が同じデータベース インスタンスを提供するようにするには、@Provides provideDatabase
メソッドに @Singleton
アノテーションを付けます。
各 Hilt コンテナには、Hilt がカスタム バインディングに依存関係として注入できるデフォルト バインディングのセットが用意されています。たとえば applicationContext
の場合、アクセスするにはフィールドに @ApplicationContext
アノテーションを付ける必要があります。
アプリの実行
これで、Hilt は LogsFragment
にインスタンスを注入するために必要なすべての情報を得ました。ただし、Hilt を適切に動作させるには、アプリを実行する前に Fragment
をホストする Activity
を Hilt に把握させる必要があります。MainActivity
に @AndroidEntryPoint
アノテーションを付ける必要があります。
ui/MainActivity.kt
ファイルを開き、MainActivity
に @AndroidEntryPoint
アノテーションを付けます。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() { ... }
これで、アプリを実行して、すべてが以前と同じように機能するかを確認できます。
次に、アプリをリファクタリングして、MainActivity
から ServiceLocator
呼び出しを削除しましょう。
MainActivity
は、provideNavigator(activity: FragmentActivity)
関数を呼び出す ServiceLocator
から AppNavigator
のインスタンスを取得します。
AppNavigator
はインターフェースのため、コンストラクタ注入を使用できません。 インターフェースに使用する実装を Hilt に指示するには、Hilt モジュール内の関数に @Binds
アノテーションを使用します。
抽象関数には @Binds
アノテーションを付ける必要があります(抽象関数であるため、コードが含まれておらず、クラスも抽象化する必要があります)。抽象関数の戻り値の型は、実装したいインターフェース(AppNavigator
)です。実装を指定するには、インターフェースの実装型(AppNavigatorImpl
)に一意のパラメータを追加します。
前に作成した DatabaseModule
クラスに情報を追加する方法や、新しいモジュールを使用する必要性を検討します。新しいモジュールを作成する理由はいくつかあります。
- モジュール名は、整理しやすいように、提供する情報のタイプを表すように指定します。たとえば、
DatabaseModule
という名前のモジュールにナビゲーション バインディングを含めるのは合理的ではありません。 DatabaseModule
モジュールはApplicationComponent
にインストールされるため、アプリケーション コンテナでバインディングを使用できます。新しいナビゲーション情報(AppNavigator
)では、Activity に固有の情報が必要です(AppNavigatorImpl
が依存関係としてActivity
を持つため)。そのため、Application
コンテナではなくActivity
コンテナ(Activity
に関する情報がある)をインストールする必要があります。- Hilt モジュールには、非静的バインディング メソッドと抽象バインディング メソッドを両方含めることはできないため、
@Binds
アノテーションと@Provides
アノテーションを同じクラスに配置することはできません。
di
フォルダ内に NavigationModule.kt
という新しいファイルを作成します。次に、NavigationModule
という新しい抽象クラスを作成し、上記のように @Module
アノテーションと @InstallIn(ActivityComponent::class)
アノテーションを付けます。
@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {
@Binds
abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}
モジュール内で AppNavigator
のバインディングを追加できます。これは抽象関数であり、Hilt に通知しているインターフェース(AppNavigator
)を返し、関数のパラメータは、そのインターフェースの実装(AppNavigatorImpl
)です。
ここで、AppNavigatorImpl
のインスタンスを提供する方法を Hilt に指示する必要があります。このクラスではコンストラクタ注入を実行できるため、コンストラクタに @Inject
アノテーションを付けるだけで完了します。
navigator/AppNavigatorImpl.kt
ファイルを開き、次の手順を行います。
class AppNavigatorImpl @Inject constructor(
private val activity: FragmentActivity
) : AppNavigator {
...
}
AppNavigatorImpl
は FragmentActivity
に依存します。AppNavigator
インスタンスは Activity
コンテナで提供されます(NavigationModule
が ActivityComponent
にインストールされているため、Fragment
コンテナと View
コンテナでも利用できます)。FragmentActivity
は、事前定義されたバインディングとして提供されており、すでに利用できます。
アクティビティでの Hilt の使用
これで、Hilt には AppNavigator
インスタンスを注入するために必要なすべての情報が含まれています。MainActivity.kt
ファイルを開いて次の手順を行います。
- Hilt が取得できるように
navigator
フィールドに@Inject
アノテーションを付ける private
の可視性修飾子を削除するonCreate
関数内のnavigator
初期化コードを削除する
新しいコードは次のようになります。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var navigator: AppNavigator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
navigator.navigateTo(Screens.BUTTONS)
}
}
...
}
アプリの実行
アプリを実行して、想定どおりに動作するかを確認します。
リファクタリングの終了
現在、ServiceLocator
を使用して依存関係を取得する唯一のクラスは ButtonsFragment
です。Hilt は、ButtonsFragment
で必要なすべての型を提供する方法をすでに把握しているため、このクラスでフィールド注入を実行するだけで完了します。
すでに説明したように、Hilt がこのクラスにフィールドを注入するには、以下を行う必要があります。
ButtonsFragment
に@AndroidEntryPoint
アノテーションを付けるlogger
フィールドとnavigator
フィールドから非公開修飾子を削除し、@Inject
アノテーションを付ける- フィールド初期化コード(
onAttach
メソッドとpopulateFields
メソッド)を削除する
ButtonsFragment
のコードは次のとおりです。
@AndroidEntryPoint
class ButtonsFragment : Fragment() {
@Inject lateinit var logger: LoggerLocalDataSource
@Inject lateinit var navigator: AppNavigator
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_buttons, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
}
}
この型のスコープはアプリケーション コンテナに設定されているため、LoggerLocalDataSource
のインスタンスは LogsFragment
で使用したインスタンスと同じです。ただし、AppNavigator
のインスタンスは MainActivity
のインスタンスと異なります。そのスコープを対応する Activity
コンテナに設定していないためです。
この時点で ServiceLocator
クラスでは依存関係が提供されなくなったため、プロジェクトから完全に削除できます。これはインスタンスを保持していた LogApplication
クラスでのみ使用します。このクラスは不要になったため、クリーンアップします。
LogApplication
クラスを開き、ServiceLocator
の使用を削除します。Application
クラスの新しいコードは次のとおりです。
@HiltAndroidApp
class LogApplication : Application()
いつでも ServiceLocator
クラスをプロジェクトから完全に削除できるようになりました。ServiceLocator
はまだテストで使用されているため、AppTest
クラスからもその使用を削除します。
これまでの基本的な内容
これまでに学んだ知識で、Android アプリで依存性注入ツールとして Hilt を使用できるはずです。
続いて、アプリに新しい機能を追加して、Hilt のより高度な機能をさまざまな状況で使用する方法を見ていきましょう。
ここまでで、プロジェクトから ServiceLocator
クラスを削除し、また Hilt の基本を学びました。次に、アプリに新しい機能を追加して、他の Hilt 機能を見てみましょう。
このセクションでは、次の内容について説明します。
- アクティビティ コンテナにスコープを設定する方法
- 修飾子の仕組み、修飾子が解決する問題、その使用方法
この部分を示すには、アプリで別の動作が必要になります。アプリのセッション中にログだけを記録する目的で、ログの保存先をデータベースからメモリ内リストに切り替えます。
LoggerDataSource インターフェース
データソースをインターフェースに抽象化してみましょう。data
フォルダの下に、LoggerDataSource.kt
という名前の新しいファイルを作成し、次の内容を含めます。
package com.example.android.hilt.data
// Common interface for Logger data sources.
interface LoggerDataSource {
fun addLog(msg: String)
fun getAllLogs(callback: (List<Log>) -> Unit)
fun removeLogs()
}
LoggerLocalDataSource
は ButtonsFragment
と LogsFragment
の両方の Fragment で使用されます。LoggerDataSource
のインスタンスを使用するには、これらをリファクタリングして使用する必要があります。
LogsFragment
を開き、logger 変数の型を LoggerDataSource
に設定します。
@AndroidEntryPoint
class LogsFragment : Fragment() {
@Inject lateinit var logger: LoggerDataSource
...
}
ButtonsFragment
についても同様に処理します。
@AndroidEntryPoint
class ButtonsFragment : Fragment() {
@Inject lateinit var logger: LoggerDataSource
...
}
次に、LoggerLocalDataSource
でこのインターフェースを実装します。data/LoggerLocalDataSource.kt
ファイルを開き、次の手順を行います。
LoggerDataSource
インターフェースを実装する- このメソッドを
override
でマークする
@Singleton
class LoggerLocalDataSource @Inject constructor(
private val logDao: LogDao
) : LoggerDataSource {
...
override fun addLog(msg: String) { ... }
override fun getAllLogs(callback: (List<Log>) -> Unit) { ... }
override fun removeLogs() { ... }
}
ここでは、LoggerInMemoryDataSource
という LoggerDataSource
の別の実装を作成します。これにより、ログがメモリに保存されます。data
フォルダの下に、LoggerInMemoryDataSource.kt
という名前の新しいファイルを作成し、次の内容を含めます。
package com.example.android.hilt.data
import java.util.LinkedList
class LoggerInMemoryDataSource : LoggerDataSource {
private val logs = LinkedList<Log>()
override fun addLog(msg: String) {
logs.addFirst(Log(msg, System.currentTimeMillis()))
}
override fun getAllLogs(callback: (List<Log>) -> Unit) {
callback(logs)
}
override fun removeLogs() {
logs.clear()
}
}
スコープの Activity コンテナへの設定
実装の詳細として LoggerInMemoryDataSource
を使用できるようにするには、この型のインスタンスを提供する方法を Hilt に指示する必要があります。以前と同様に、クラス コンストラクタに @Inject
アノテーションを付けます。
class LoggerInMemoryDataSource @Inject constructor(
) : LoggerDataSource { ... }
アプリには Activity が 1 つしか含まれていない(シングル Activity アプリとも呼ばれる)ため、Activity
コンテナ内に LoggerInMemoryDataSource
のインスタンスを作成し、そのインスタンスを Fragment
で再利用する必要があります。
LoggerInMemoryDataSource
のスコープを Activity
コンテナに設定することで、メモリ内のログの動作を実現できます。Activity
が作成されるたびに、独自のコンテナ(異なるインスタンス)が作成されます。各コンテナで、logger を依存関係として、またはフィールド注入用に使用する必要がある場合、LoggerInMemoryDataSource
の同じインスタンスが提供されます。また、コンポーネント階層の下のコンテナにも、同じインスタンスが提供されます。
スコープをコンポーネントに設定するドキュメントに従って、特定の型のスコープを Activity
コンテナに設定するには、型に @ActivityScoped
アノテーションを付ける必要があります。
@ActivityScoped
class LoggerInMemoryDataSource @Inject constructor(
) : LoggerDataSource { ... }
現時点で、Hilt は LoggerInMemoryDataSource
と LoggerLocalDataSource
のインスタンスを提供する方法を把握していますが、LoggerDataSource
はどうでしょうか。LoggerDataSource
がリクエストされたときにどの実装を使用すべきかを Hilt は把握していません。
前のセクションで説明したように、モジュール内で @Binds
アノテーションを使用して、使用する実装を Hilt に指示できます。ただし、同じプロジェクトで両方の実装を提供する必要がある場合はどうなるでしょうか。たとえば、アプリの実行中に LoggerInMemoryDataSource
を使用し、Service
で LoggerLocalDataSource
を使用する場合があります。
同じインターフェースの 2 つの実装
di
フォルダに LoggingModule.kt
という新しいファイルを作成しましょう。LoggerDataSource
の各実装のスコープは異なるコンテナに設定されているため、同じモジュールを使用することはできません。LoggerInMemoryDataSource
のスコープは Activity
コンテナに設定され、LoggerLocalDataSource
のスコープは Application
コンテナに設定されます。
幸い、先ほど作成したファイル内で両方のモジュールのバインディングを定義できます。
package com.example.android.hilt.di
@InstallIn(ApplicationComponent::class)
@Module
abstract class LoggingDatabaseModule {
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
型がスコープ設定されている場合、@Binds
メソッドにはスコープ設定アノテーションが必要です。そのため、上記の関数には @Singleton
アノテーションと @ActivityScoped
アノテーションが付けられます。@Binds
または @Provides
が型のバインディングとして使用されている場合、その型のスコープ設定アノテーションは使用されなくなるため、他の実装クラスから削除できます。
この時点でプロジェクトをビルドしようとすると、DuplicateBindings
エラーが表示されます。
error: [Dagger/DuplicateBindings] com.example.android.hilt.data.LoggerDataSource is bound multiple times
これは、LoggerDataSource
型が Fragment
に注入されていますが、同じ型に対して 2 つのバインディングがあり、Hilt はどの実装を使用すべきか把握していないためです。Hilt が使用する方法を把握する方法は、次のとおりです。
修飾子の使用
同じ型の異なる実装(複数のバインディング)を提供する方法を Hilt に指示するには、修飾子を使用します。
各修飾子はバインディングを識別するために使用されるため、実装ごとに修飾子を定義する必要があります。この型を Android クラスに注入する場合や、他のクラスの依存関係として利用する場合、あいまいさを回避するために、修飾子アノテーションを使用する必要があります。
修飾子は単なるアノテーションであるため、モジュールを追加した LoggingModule.kt
ファイルで次のように定義できます。
package com.example.android.hilt.di
@Qualifier
annotation class InMemoryLogger
@Qualifier
annotation class DatabaseLogger
次に、これらの修飾子を使用して、各実装を提供する @Binds
(または必要な場合は @Provides
)関数にアノテーションを付ける必要があります。完全なコードを確認し、@Binds
メソッドに修飾子が使用されていることに注意してください。
package com.example.android.hilt.di
@Qualifier
annotation class InMemoryLogger
@Qualifier
annotation class DatabaseLogger
@InstallIn(ApplicationComponent::class)
@Module
abstract class LoggingDatabaseModule {
@DatabaseLogger
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@InMemoryLogger
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
また、注入する場合、これらの修飾子は注入する実装に使用する必要があります。この例では、Fragment
で LoggerInMemoryDataSource
の実装を使用します。
LogsFragment
を開き、logger フィールドに @InMemoryLogger
修飾子を使用して、LoggerInMemoryDataSource
のインスタンスを注入するよう Hilt に指示します。
@AndroidEntryPoint
class LogsFragment : Fragment() {
@InMemoryLogger
@Inject lateinit var logger: LoggerDataSource
...
}
ButtonsFragment
についても同様にします。
@AndroidEntryPoint
class ButtonsFragment : Fragment() {
@InMemoryLogger
@Inject lateinit var logger: LoggerDataSource
...
}
使用するデータベース実装を変更するには、注入されたフィールドに @InMemoryLogger
アノテーションではなく @DatabaseLogger
アノテーションを付けるだけです。
アプリの実行
アプリを実行し、ボタン操作を通じて対応するログが「See all logs」画面に表示されるかどうかを見て、これまでの作業の結果を確認できます。
ログはデータベースに保存されなくなることに注意してください。異なるセッション間でログが保持されることはありません。アプリを閉じてもう一度開くとログ画面が空白になります。
アプリが Hilt に完全に移行されたので、プロジェクト内にあるインストルメンテーション テストも移行できるようになりました。このテストは、アプリの機能をチェックするためのものであり、app/androidTest
フォルダの AppTest.kt
ファイルに配置されています。これを開きます。
プロジェクトから ServiceLocator
クラスを削除したため、テストをコンパイルできないことがわかります。クラスから @After tearDown
メソッドを削除して、不要になった ServiceLocator
への参照を削除します。
androitTest
テストをエミュレータで実行します。happyPath
テストでは、「Button 1」のタップがデータベースに記録されたことを確認します。アプリがメモリ内データベースを使用しているため、テストが終了すると、すべてのログが表示されなくなります。
Hilt を使用した UI テスト
Hilt は、本番環境のコードと同様に、UI テストに依存関係を注入します。
Hilt を使用したテストでは、テストごとに新しいコンポーネントのセットが自動的に生成されるため、メンテナンスは不要です。
テストの依存関係の追加
Hilt ではテストに役立つ、テスト固有のアノテーションを含む追加のライブラリ(hilt-android-testing
と呼ばれる)を使用しており、これをプロジェクトに追加する必要があります。また、Hilt は androidTest
フォルダにクラスのコードを生成する必要があるため、アノテーション プロセッサがそこで動作できる必要があります。この設定を有効にするには、app/build.gradle
ファイルに 2 つの依存関係を含める必要があります。
これらの依存関係を追加するには、app/build.gradle
を開き、dependencies
セクションの一番下に次の設定を追加します。
...
dependencies {
// Hilt testing dependency
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
// Make Hilt generate code in the androidTest folder
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
}
カスタム TestRunner
Hilt を使用したインストゥルメント化テストは、Hilt をサポートする Application
で実行する必要があります。このライブラリには、UI テストの実行に使用できる HiltTestApplication
がすでに用意されています。テストで使用する Application
を指定するには、プロジェクト内に新しいテストランナーを作成します。
同じレベルで、AppTest.kt
ファイルは androidTest
フォルダの下にあり、CustomTestRunner
という名前の新しいファイルが作成されます。CustomTestRunner
は AndroidJUnitRunner から拡張され、次のように実装されます。
class CustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
次に、このテストランナーをインストルメンテーション テストに使用するようプロジェクトに指示する必要があります。これは、app/build.gradle
ファイルの testInstrumentationRunner
属性で指定します。ファイルを開き、デフォルトの testInstrumentationRunner
コンテンツを次のように置き換えます。
...
android {
...
defaultConfig {
...
testInstrumentationRunner "com.example.android.hilt.CustomTestRunner"
}
...
}
...
これで、UI テストで Hilt を使用できるようになりました。
Hilt を使用したテストの実行
エミュレータ テストクラスで Hilt を使用するには、以下を行う必要があります。
@HiltAndroidTest
アノテーションを付ける。このアノテーションは、各テストに Hilt コンポーネントを生成させるものです。HiltAndroidRule
を使用する。このルールは、コンポーネントの状態を管理し、テストの注入に使用されるものです。
AppTest
に以下を追加しましょう。
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
class AppTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
...
}
クラス定義またはテストメソッドの定義の横にある再生ボタンを使用してテストを実行すると、エミュレータが起動されます。エミュレータを構成していればテストに合格します。
フィールド注入やテストのバインディングの置き換えなど、テストと機能について詳しくは、こちらのドキュメントをご覧ください。
この Codelab のセクションでは、Hilt でサポートされていないクラスに依存関係を注入するために使用される @EntryPoint
アノテーションの使用方法を学習します。
前述のように、Hilt ではほとんどの一般的な Android コンポーネントがサポートされています。ただし、Hilt で直接サポートされていないクラスや Hilt を使用できないクラスでは、フィールド注入を実行する必要があります。
このような場合は、@EntryPoint
を使用します。エントリ ポイントとは、Hilt を使用して依存関係を注入できないコードから Hilt が提供するオブジェクトを取得できる境界の場所です。コード内で Hilt で管理するコンテナが開始される最初のポイントです。
ユースケース
アプリケーション プロセス以外でログをエクスポートできるようにしましょう。そのためには、ContentProvider
を使用する必要があります。ここでは、ユーザーが ContentProvider
を使用して、特定のログ(id
を指定)またはアプリ内のすべてのログをクエリすることだけを許可します。データの取得には Room データベースを使用します。したがって LogDao
クラスは、データベース Cursor
を使用して必要な情報を返すメソッドを提供する必要があります。LogDao.kt
ファイルを開き、次のメソッドをインターフェースに追加します。
@Dao
interface LogDao {
...
@Query("SELECT * FROM logs ORDER BY id DESC")
fun selectAllLogsCursor(): Cursor
@Query("SELECT * FROM logs WHERE id = :id")
fun selectLogById(id: Long): Cursor?
}
次に、新しい ContentProvider
クラスを作成し、query
メソッドをオーバーライドして、ログを含む Cursor
を返す必要があります。新しい contentprovider
ディレクトリの下に LogsContentProvider.kt
という新しいファイルを作成し、次の内容を含めます。
package com.example.android.hilt.contentprovider
import android.content.ContentProvider
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.content.UriMatcher
import android.database.Cursor
import android.net.Uri
import com.example.android.hilt.data.LogDao
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ApplicationComponent
import java.lang.UnsupportedOperationException
/** The authority of this content provider. */
private const val LOGS_TABLE = "logs"
/** The authority of this content provider. */
private const val AUTHORITY = "com.example.android.hilt.provider"
/** The match code for some items in the Logs table. */
private const val CODE_LOGS_DIR = 1
/** The match code for an item in the Logs table. */
private const val CODE_LOGS_ITEM = 2
/**
* A ContentProvider that exposes the logs outside the application process.
*/
class LogsContentProvider: ContentProvider() {
private val matcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, LOGS_TABLE, CODE_LOGS_DIR)
addURI(AUTHORITY, "$LOGS_TABLE/*", CODE_LOGS_ITEM)
}
override fun onCreate(): Boolean {
return true
}
/**
* Queries all the logs or an individual log from the logs database.
*
* For the sake of this codelab, the logic has been simplified.
*/
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
val code: Int = matcher.match(uri)
return if (code == CODE_LOGS_DIR || code == CODE_LOGS_ITEM) {
val appContext = context?.applicationContext ?: throw IllegalStateException()
val logDao: LogDao = getLogDao(appContext)
val cursor: Cursor? = if (code == CODE_LOGS_DIR) {
logDao.selectAllLogsCursor()
} else {
logDao.selectLogById(ContentUris.parseId(uri))
}
cursor?.setNotificationUri(appContext.contentResolver, uri)
cursor
} else {
throw IllegalArgumentException("Unknown URI: $uri")
}
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
throw UnsupportedOperationException("Only reading operations are allowed")
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
throw UnsupportedOperationException("Only reading operations are allowed")
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
throw UnsupportedOperationException("Only reading operations are allowed")
}
override fun getType(uri: Uri): String? {
throw UnsupportedOperationException("Only reading operations are allowed")
}
}
getLogDao(appContext)
呼び出しはコンパイルされないことがわかります。これは、Hilt アプリケーション コンテナから LogDao
の依存関係を取得することで実装する必要があります。ただし Hilt では、ContentProvider
への注入に対してすぐに使用できるサポートを提供していません。Activity の場合、@AndroidEntryPoint
の使用などのサポートを提供します。
このクラスにアクセスするには、@EntryPoint
アノテーションを使用して新しいインターフェースを作成する必要があります。
@EntryPoint の使い方
エントリ ポイントは、必要なバインディング タイプのアクセサ メソッドをすべて含むインターフェースです(修飾子を含む)。また、エントリ ポイントをインストールするコンポーネントを指定するには、インターフェースに @InstallIn
アノテーションを付ける必要があります。
エントリ ポイント インターフェースを使用するクラスに新しいインターフェースを追加することをおすすめします。このため、このインターフェースを LogsContentProvider.kt
ファイルに追加します。
class LogsContentProvider: ContentProvider() {
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface LogsContentProviderEntryPoint {
fun logDao(): LogDao
}
...
}
Application
コンテナのインスタンスからの依存関係が必要であるため、このインターフェースには @EntryPoint
アノテーションが付けられ、ApplicationComponent
にインストールされます。インターフェース内で、アクセスするバインディングのメソッド(この例では LogDao
)を提供します。
エントリ ポイントにアクセスするには、EntryPointAccessors
の適切な静的メソッドを使用します。パラメータは、コンポーネント インスタンスか、コンポーネント ホルダーとして機能する @AndroidEntryPoint
オブジェクトのいずれかにします。パラメータとして渡すコンポーネントと EntryPointAccessors
静的メソッドの両方を、@EntryPoint
インターフェースの @InstallIn
アノテーションで指定した Android クラスと一致させてください。
これで、上記のコードに含まれていない getLogDao
メソッドを実装できます。上記の LogsContentProviderEntryPoint
クラスで定義されたエントリ ポイント インターフェースを使用してみましょう。
class LogsContentProvider: ContentProvider() {
...
private fun getLogDao(appContext: Context): LogDao {
val hiltEntryPoint = EntryPointAccessors.fromApplication(
appContext,
LogsContentProviderEntryPoint::class.java
)
return hiltEntryPoint.logDao()
}
}
applicationContext
が静的 EntryPoints.get
メソッドや @EntryPoint
アノテーションが付いたインターフェースのクラスにどのように渡されるかに注目してください。
これで、Hilt について理解し、Android アプリに追加できるようになりました。このコードラボでは次の内容を学びました。
@HiltAndroidApp
を使用してアプリケーション クラスに Hilt を設定する方法@AndroidEntryPoint
を使用して各種 Android ライフサイクル コンポーネントに依存関係コンテナを追加する方法- モジュールを使用して、特定の型を提供する方法を Hilt に指示する方法
- 修飾子を使用して特定の型に複数のバインディングを提供する方法
- Hilt を使用してアプリをテストする方法
@EntryPoint
の用途と使用方法