この Codelab では、Android アプリへの依存関係注入(DI)のために Dagger を Hilt に移行する方法について説明します。この Codelab では、Android アプリで Dagger を使用する方法に関する Codelab を Hilt に移行します。この Codelab の目的は、移行の計画を立てる方法と、アプリの機能を維持したまま、Dagger と Hilt を共存させた状態で、各 Dagger コンポーネントを Hilt に移行してゆく方法を説明することです。
依存関係注入により、コードの再利用、リファクタリング、テストが簡単になります。Hilt は、よく知られた DI ライブラリである Dagger の上に構築されているため、コンパイル時の正確性、実行時のパフォーマンス、スケーラビリティ、Android Studio のサポートといった Dagger の恩恵を受けられます。
多くの Android フレームワーク クラスは OS 自体によってインスタンス化されるため、Android アプリで Dagger を使用する際には、関連するボイラープレートがあります。Hilt では、以下が自動的に生成、提供されることで、このボイラープレートの多くが除去されています。
- Dagger を使用して Android フレームワークのクラスを統合するためのコンポーネント(手動で作成する必要がなくなります)。
- Hilt が自動的に生成するコンポーネントのためのスコープ アノテーション。
- 事前定義済みのバインディングと修飾子。
なによりも、Dagger と Hilt は共存できるため、必要になったときに随時アプリの移行ができます。
この Codelab で問題(コードのバグ、文法的な誤り、不明確な表現など)が見つかった場合は、Codelab の左下隅にある [誤りを報告] から問題を報告してください。
前提条件
- Kotlin の構文に関する理解
- Dagger の利用経験
学習内容
- Android アプリに Hilt を追加する方法。
- 移行戦略を計画する方法。
- 既存の Dagger コードの動作を維持したまま、コンポーネントを Hilt に移行する方法。
- スコープ設定されたコンポーネントを移行する方法。
- Hilt を使用してアプリをテストする方法。
必要なもの
- Android Studio 4.0 以降。
コードを取得する
次のコマンドで、GitHub から Codelab のコードを取得します。
$ git clone https://github.com/googlecodelabs/android-dagger-to-hilt
次のボタンで、リポジトリを ZIP ファイルとしてダウンロードすることもできます。
Android Studio を開く
Android Studio をダウンロードする必要がある場合は、こちらからダウンロードします。
プロジェクトの設定
このプロジェクトには複数の GitHub ブランチがあります。
master
は、チェックアウトまたはダウンロードしたブランチです。この Codelab の出発点となります。interop
は、Dagger と Hilt の相互運用ブランチです。solution
には、この Codelab の解答に加え、テストと ViewModel もあります。
master
ブランチから Codelab の手順に沿って一歩一歩マイペースで進めることをおすすめします。
Codelab の途中には、プロジェクトに追加する必要があるコード スニペットを記載しています。また、コード スニペットのコメントで明示的に記載されたコードを削除する必要がある場合もあります。
チェックポイントとして、特定のステップで助けが必要になった場合に利用できる中間ブランチがあります。
git を使用して solution
ブランチを取得するには、次のコマンドを使用します。
$ git clone -b solution https://github.com/googlecodelabs/android-dagger-to-hilt
または、次の場所から解答コードをダウンロードします。
よくある質問
サンプルアプリの実行
まず、サンプルアプリがどんなものか見てみましょう。次の手順に沿って、Android Studio でサンプルアプリを開いてください。
- zip アーカイブをダウンロードした場合は、ファイルをローカルに展開します。
- Android Studio でプロジェクトを開きます。
- [Run] ボタンをクリックして、エミュレータを選択するか、Android デバイスを接続します。すると、登録画面が表示されます。
このアプリには、Dagger を使用するフローが 4 個あります(フローはアクティビティとして実装されています)。
- Registration: ユーザー名とパスワードを入力し、利用規約に同意して登録します。
- Login: Registration フローで追加された認証情報でログインします。登録の解除もできます。
- Home: ホーム画面です。未読通知の数が表示されます。
- Settings: ログアウトと、未読通知数の更新ができます(通知数には乱数が生成されます)。
プロジェクトは一般的な MVVM パターンに従っているため、ビューの複雑な部分はすべて ViewModel が請け負います。このプロジェクトの構造をよく理解しておいてください。
矢印はオブジェクト同士の依存関係を表しています。これは、アプリケーション グラフと呼ばれるもので、アプリのすべてのクラスと、それらの間の依存関係を表しています。
master
ブランチ内のコードでは、Dagger を使用して依存関係を注入しています。これから、アプリをリファクタリングして、Component を手動で作成する代わりに、Hilt が Component と他の Dagger 関連のコードを生成するようにします。
このアプリ内での Dagger の設定は、次の図のようになっています。型に付いた丸印は、その型がその型を提供する Component にスコープ設定されていることを表します。
簡単にするために、最初にダウンロードした master
ブランチのこのプロジェクトには、Hilt の依存関係を追加してあります。そのため、以下のコードを追加する必要はありません。それでは、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/build.gradle
を開き、上の kotlin-kapt
プラグインのすぐ下にある Hilt Gradle プラグインの宣言を確認してください。
...
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 を使ってみましょう。
一度にすべてを Hilt に移行することも頭に浮かぶかもしれませんが、現実のプロジェクトでは、少しずつ Hilt に移行しながら、その都度アプリのビルドと実行を行ってエラーがないことを確認したくなるでしょう。
Hilt に移行する際には、作業を複数のステップに分けることをおすすめします。おすすめは、Application または @Singleton
コンポーネントの移行から始めて、その後でアクティビティとフラグメントを移行する方法です。
この Codelab では、まず AppComponent
を移行してから、Registration、Login、最後に Main と Settings の順で各フローを移行します。
移行する中で、すべての @Component
インターフェースと @Subcomponent
インターフェースを削除し、すべてのモジュールに @InstallIn
のアノテーションを付けます。
移行後は、すべての Application
/Activity
/Fragment
/View
/Service
/BroadcastReceiver
クラスに @AndroidEntryPoint
のアノテーションを付け、コンポーネントのインスタンス化または伝播を行うすべてのコードを削除する必要があります。
移行を計画するため、まず AppComponent.kt
から始めて、コンポーネントの階層を確認しましょう。
@Singleton
// Definition of a Dagger component that adds info from the different modules to the graph
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
// Factory to create instances of the AppComponent
@Component.Factory
interface Factory {
// With @BindsInstance, the Context passed in will be available in the graph
fun create(@BindsInstance context: Context): AppComponent
}
// Types that can be retrieved from the graph
fun registrationComponent(): RegistrationComponent.Factory
fun loginComponent(): LoginComponent.Factory
fun userManager(): UserManager
}
AppComponent
には、@Component
のアノテーションが付けられていて、StorageModule
と AppSubcomponents
の 2 つのモジュールがあります。
AppSubcomponents
には、RegistrationComponent
、LoginComponent
、UserComponent
の 3 つのコンポーネントがあります。
LoginComponent
はLoginActivity
に注入されています。RegistrationComponent
はRegistrationActivity
、EnterDetailsFragment
、TermsAndConditionsFragment
に注入されています。また、このコンポーネントはRegistrationActivity
にスコープが設定されています。
UserComponent は MainActivity
と SettingsActivity
に注入されています。
ApplicationComponent
への参照は、アプリに移行する Component にマッピングされる Hilt が生成した Component(生成されたすべてのコンポーネントへのリンク)に置き換えることができます。
このセクションでは、AppComponent
を移行します。以降のステップでは各コンポーネントを Hilt に移行しますが、その間も既存の Dagger コードの動作を維持するには、下準備が必要となります。
Hilt を初期化してコード生成を開始するには、Application
クラスに Hilt のアノテーションを付ける必要があります。
MyApplication.kt
を開き、@HiltAndroidApp
アノテーションをクラスに追加してください。これらのアノテーションは、Dagger が検出してアノテーション プロセッサで使用するコード生成をトリガーするよう Hilt に指示します。
MyApplication.kt
package com.example.android.dagger
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
open class MyApplication : Application() {
// Instance of the AppComponent that will be used by all the Activities in the project
val appComponent: AppComponent by lazy {
initializeComponent()
}
open fun initializeComponent(): AppComponent {
// Creates an instance of AppComponent using its Factory constructor
// We pass the applicationContext that will be used as Context in the graph
return DaggerAppComponent.factory().create(applicationContext)
}
}
1. Component モジュールを移行する
まず、AppComponent.kt を開いてください。AppComponent
には、@Component
アノテーションで追加された 2 つのモジュール(StorageModule
と AppSubcomponents
)があります。まず、これらの 2 つのモジュールを移行して、Hilt がそれらのモジュールを生成される ApplicationComponent
に追加するようにします。
そのために、AppSubcomponents.kt を開き、このクラスに @InstallIn
アノテーションを付けます。@InstallIn
アノテーションでは、正しいコンポーネントにモジュールが追加されるようにパラメータを指定します。今回は、アプリケーション レベルのコンポーネントを移行しようとしているので、ApplicationComponent
にバインディングが生成されるようにします。
AppSubcomponents.kt
// This module tells a Component which are its subcomponents
// Install this module in Hilt-generated ApplicationComponent
@InstallIn(ApplicationComponent::class)
@Module(
subcomponents = [
RegistrationComponent::class,
LoginComponent::class,
UserComponent::class
]
)
class AppSubcomponents
StorageModule
でも同じ変更を加える必要があります。StorageModule.kt を開き、前の手順と同じように @InstallIn
アノテーションを追加してください。
StorageModule.kt
// Tells Dagger this is a Dagger module
// Install this module in Hilt-generated ApplicationComponent
@InstallIn(ApplicationComponent::class)
@Module
abstract class StorageModule {
// Makes Dagger provide SharedPreferencesStorage when a Storage type is requested
@Binds
abstract fun provideStorage(storage: SharedPreferencesStorage): Storage
}
@InstallIn
アノテーションで、Hilt が生成した ApplicationComponent にモジュールを追加するよう Hilt に指示しました。
戻って AppComponent.kt を確認しましょう。AppComponent
は、RegistrationComponent
、LoginComponent
、UserManager
に依存関係を提供しています。次のステップでは、これらのコンポーネントを移行する準備をします。
2. 公開されている型を移行する
アプリをすべて Hilt に移行する間、Hilt ではエントリ ポイントを使って Dagger の依存関係を手動で要求する必要があります。エントリ ポイントを使用すると、各 Dagger コンポーネントを移行しながら、アプリの動作を維持することができます。このステップでは、各 Dagger コンポーネントを置き換え、Hilt が生成した ApplicationComponent
内で依存関係を手動でルックアップするようにします。
Hilt が生成した ApplicationComponent
から RegistrationActivity.kt
の RegistrationComponent.Factory
を取得するには、@InstallIn
アノテーションを付けた EntryPoint インターフェースを新たに作成する必要があります。InstallIn
アノテーションにより、バインディングの取得元を Hilt に指示します。エントリ ポイントにアクセスするには、EntryPointAccessors
の適切な静的メソッドを使用します。パラメータは、コンポーネント インスタンスか、コンポーネント ホルダーとして機能する @AndroidEntryPoint
オブジェクトのいずれかです。
RegistrationActivity.kt
class RegistrationActivity : AppCompatActivity() {
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface RegistrationEntryPoint {
fun registrationComponent(): RegistrationComponent.Factory
}
...
}
ここでは、Dagger 関連のコードを、RegistrationEntryPoint
で置き換える必要があります。registrationComponent
の初期化を変更して、RegistrationEntryPoint
を使用します。移行して Hilt が使用されるようになるまでは、この変更により RegistrationActivity
が Hilt の生成するコードから自身の依存関係にアクセスできます。
RegistrationActivity.kt
// Creates an instance of Registration component by grabbing the factory from the app graph
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, RegistrationEntryPoint::class.java)
registrationComponent = entryPoint.registrationComponent().create()
Component の他の公開されている型についても、同じ下準備が必要です。まず、LoginComponent.Factory
から始めましょう。LoginActivity
を開き、先程と同じように @InstallIn
と @EntryPoint
のアノテーションが付いた LoginEntryPoint
インターフェースを作成します。ただし、LoginActivity
が Hilt コンポーネントで必要とするものを公開します。
LoginActivity.kt
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface LoginEntryPoint {
fun loginComponent(): LoginComponent.Factory
}
Hilt が LoginComponent
の提供方法を認識するようになったので、古い inject()
呼び出しを EntryPoint の loginComponent()
に置き換えます。
LoginActivity.kt
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, LoginEntryPoint::class.java)
entryPoint.loginComponent().create().inject(this)
AppComponent
から公開されている 3 つの型のうちの 2 つが、Hilt EntryPoint で処理されるように置き換えられました。次に、UserManager
に同様の変更を加える必要があります。RegistrationComponent
と LoginComponent
とは異なり、UserManager
は MainActivity
と SettingsActivity
の両方で使用されています。EntryPoint インターフェースを作成する必要があるのは 1 回だけです。アノテーションが付けられた EntryPoint インターフェースは両方の Activity で使用できます。これを簡単にするために、このインターフェースを MainActivity で宣言します。
UserManagerEntryPoint
インターフェースを作成するために、MainActivity.kt
を開き、@InstallIn
と @EntryPoint
のアノテーションを付けてください。
MainActivity.kt
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface UserManagerEntryPoint {
fun userManager(): UserManager
}
ここでは、UserManagerEntryPoint
を使用するように UserManager
を変更します。
MainActivity.kt
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, UserManagerEntryPoint::class.java)
val userManager = entryPoint.userManager()
SettingsActivity.
にも同じ変更を加える必要があります。SettingsActivity.kt
を開いて、UserManager
の注入方法を置き換えます。
SettingsActivity.kt
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, MainActivity.UserManagerEntryPoint::class.java)
val userManager = entryPoint.userManager()
3. コンポーネント ファクトリを削除する
@BindsInstance
を使用して Dagger コンポーネントに Context
を渡すのは一般的なパターンです。Hilt では、Context
が事前定義のバインディングとしてすでに使用可能になっているため、これは不要です。
通常、リソース、データベース、共有設定などにアクセスするには Context
が必要です。Hilt では、修飾子 @ApplicationContext
、@ActivityContext
を使用することでコンテキストへの注入が簡略化されています。
アプリを移行する間、どの型が依存関係として Context
を必要としているかを確認し、Hilt が提供するものに置換します。
この場合、SharedPreferencesStorage
には依存関係として Context
が含まれています。Hilt にこのコンテキストを注入するよう指示するために、次を開いてください: SharedPreferencesStorage.kt. SharedPreferences
にはアプリケーションの Context
が必要なため、コンテキスト パラメータに @ApplicationContext
アノテーションを追加してください。
SharedPreferencesStorage.kt
class SharedPreferencesStorage @Inject constructor(
@ApplicationContext context: Context
) : Storage {
//...
4. 注入メソッドを移行する
次に、コンポーネントのコードの inject()
メソッドを確認し、対応するクラスに @AndroidEntryPoint
アノテーションを付ける必要があります。この例では、AppComponent
に inject()
メソッドがないため、何もする必要はありません。
5. AppComponent クラスを削除する
AppComponent.kt
に列挙されているすべてのコンポーネントには EntryPoint をすでに追加しているため、AppComponent.kt
は削除できます。
6. 移行する Component を使用しているコードを削除する
アプリケーション クラスでカスタムの AppComponent
を初期化するコードは不要になり、代わりに、Application クラスは Hilt が生成した ApplicationComponent を使用します。クラス本体のコードをすべて削除してください。削除後のコードは次のようになります。
MyApplication.kt
package com.example.android.dagger
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
open class MyApplication : Application()
これにより、Hilt が Application に追加され、AppComponent
が削除され、Hilt が生成した AppComponent に依存関係を注入するように Dagger コードが変更されました。デバイスやエミュレータでアプリをビルドして試すと、以前と同じように動作するはずです。以降のセクションで、各 Activity と各 Fragment を Hilt を使用するように移行します。
ここまでで、Application コンポーネントを移行し、下準備を行ったので、各 Component を 1 つずつ Hilt に移行できるようになりました。
まず、ログインフローの移行から始めましょう。LoginComponent
を手動で作成して LoginActivity
で使用するのではなく、それを Hilt が代わりに行うようにします。
前のセクションで使用した手順と同じですが、今回は Hilt が生成した ActivityComponent
を使用します。これは、Activity が管理する Component を移行しようとしているためです。
最初は LoginComponent.kt ですが、LoginComponent
にはモジュールがないため、何もする必要はありません。Hilt に LoginActivity
のためのコンポーネントを生成して注入させるには、アクティビティに @AndroidEntryPoint
アノテーションを付ける必要があります。
LoginActivity.kt
@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {
//...
}
LoginActivity
を Hilt に移行するために必要なコードは以上ですが、Hilt が Dagger 関連のコードを生成するため、クリーンアップが必要になります。LoginEntryPoint
インターフェースを削除してください。
LoginActivity.kt
//Remove
//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface LoginEntryPoint {
// fun loginComponent(): LoginComponent.Factory
//}
次に、onCreate()
内の EntryPoint コードを削除します。
LoginActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
//Remove
//val entryPoint = EntryPoints.get(applicationContext, LoginActivity.LoginEntryPoint::class.java)
//entryPoint.loginComponent().create().inject(this)
super.onCreate(savedInstanceState)
...
}
Hilt がコンポーネントを生成するので、LoginComponent.kt を見つけて削除してください。
現在、LoginComponent
は AppSubcomponents.kt にサブコンポーネントとして列挙されています。Hilt がバインディングを生成してくれるため、サブコンポーネント リストから LoginComponent
を削除しても問題ありません。
AppSubcomponents.kt
// This module tells a Component which are its subcomponents
@InstallIn(ApplicationComponent::class)
@Module(
subcomponents = [
RegistrationComponent::class,
UserComponent::class
]
)
class AppSubcomponents
ここまでで、LoginActivity
が移行されて Hilt を使用するようになります。このセクションでは、追加したコードよりも削除したコードの方がはるかに多くなっています。これは素晴らしいことです。Hilt の使用時に入力するコードが少ないだけでなく、保守が必要となるコードと、バグを生む可能性のあるコードが少なくなります。
このセクションでは、登録フローを移行してゆきます。移行の計画を行うために、RegistrationComponent
を確認しましょう。RegistrationComponent.kt を開き、inject() 関数まで下にスクロールしてください。依存関係を RegistrationActivity
、EnterDetailsFragment
、TermsAndConditionsFragment
に注入するのは RegistrationComponent
です。
まず、RegistrationActivity
の移行から始めましょう。RegistrationActivity.kt を開き、クラスに @AndroidEntryPoint
アノテーションを付けます。
RegistrationActivity.kt
@AndroidEntryPoint
class RegistrationActivity : AppCompatActivity() {
//...
}
RegistrationActivity
が Hilt に登録されたので、RegistrationEntryPoint
インターフェースと EntryPoint 関連のコードを onCreate()
関数から削除します。
RegistrationActivity.kt
//Remove
//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface RegistrationEntryPoint {
// fun registrationComponent(): RegistrationComponent.Factory
//}
override fun onCreate(savedInstanceState: Bundle?) {
//Remove
//val entryPoint = EntryPoints.get(applicationContext, RegistrationEntryPoint::class.java)
//registrationComponent = entryPoint.registrationComponent().create()
registrationComponent.inject(this)
super.onCreate(savedInstanceState)
//..
}
コンポーネントの生成と依存関係の注入は Hilt が行うため、registrationComponent
変数と、削除された Dagger コンポーネントに対する inject 呼び出しを削除できます。
RegistrationActivity.kt
// Remove
// lateinit var registrationComponent: RegistrationComponent
override fun onCreate(savedInstanceState: Bundle?) {
//Remove
//registrationComponent.inject(this)
super.onCreate(savedInstanceState)
//..
}
次に、EnterDetailsFragment.kt を開いてください。RegistrationActivity
と同じように、EnterDetailsFragment
に @AndroidEntryPoint
のアノテーションを付けます。
EnterDetailsFragment.kt
@AndroidEntryPoint
class EnterDetailsFragment : Fragment() {
//...
}
Hilt が依存関係を提供するので、削除された Dagger コンポーネントの inject()
呼び出しは不要です。onAttach()
関数を削除します。
次のステップは TermsAndConditionsFragment
の移行です。TermsAndConditionsFragment.kt を開き、前のステップと同じように、クラスにアノテーションを付け、onAttach()
関数を削除します。最終的なコードは次のようになります。
TermsAndConditionsFragment.kt
@AndroidEntryPoint
class TermsAndConditionsFragment : Fragment() {
@Inject
lateinit var registrationViewModel: RegistrationViewModel
//override fun onAttach(context: Context) {
// super.onAttach(context)
//
// // Grabs the registrationComponent from the Activity and injects this Fragment
// (activity as RegistrationActivity).registrationComponent.inject(this)
//}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_terms_and_conditions, container, false)
view.findViewById<Button>(R.id.next).setOnClickListener {
registrationViewModel.acceptTCs()
(activity as RegistrationActivity).onTermsAndConditionsAccepted()
}
return view
}
}
この変更により、RegistrationComponent
に列挙されているアクティビティとフラグメントはすべて移行されたので、RegistrationComponent.kt は削除できるようになりました。
RegistrationComponent
を削除すると、AppSubcomponents
のサブコンポーネント リストからの参照を削除する必要があります。
AppSubcomponents.kt
@InstallIn(ApplicationComponent::class)
// This module tells a Component which are its subcomponents
@Module(
subcomponents = [
UserComponent::class
]
)
class AppSubcomponents
登録フローの移行完了まで、あと一歩です。登録フローでは、独自のスコープ ActivityScope
を宣言して使用します。スコープは、依存関係のライフサイクルを制御します。この場合、ActivityScope
は、RegistrationActivity
から始まるフローに RegistrationViewModel
の同じインスタンスを注入するように指示します。Hilt には、これをサポートするためのライフサイクル スコープが組み込まれています。
RegistrationViewModel
を開き、@ActivityScope
アノテーションを、Hilt が提供する @ActivityScoped
に置き換えてください。
RegistrationViewModel.kt
@ActivityScoped
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
//...
}
ActivityScope
は他で使用されていないので、ActivityScope.kt は削除しても問題ありません。
ここで、アプリを実行して、Registration フローを試してみましょう。現在のユーザー名とパスワードでログインしたり、登録を解除してから新しいアカウントで再登録したりして、このフローが以前と同じように機能することを確認します。
この時点で、Dagger と Hilt がアプリ内に共存しています。Hilt は、UserManager
以外の依存関係をすべて注入しています。次のセクションでは、UserManager
を移行して、Dagger から Hilt に完全に移行します。
ここまでの Codelab で、1 つのコンポーネント UserComponent
を除いて、サンプルアプリの大部分を Hilt に移行できました。UserComponent
には、カスタム スコープである @LoggedUserScope
アノテーションが付けられています。つまり、UserComponent
が、UserManager
の同じインスタンスを @LoggedUserScope
アノテーションが付いたクラスに注入するということです。
UserComponent
は、ライフサイクルを Android クラスで管理されていないため、使用可能な Hilt コンポーネントにマッピングされません。生成された Hilt 階層の途中にカスタム コンポーネントを追加することはできないため、次の 2 つのいずれかの方法で対処します。
- 現在のプロジェクトの状態のままで、Hilt と Dagger を共存させる。
- スコープ設定されたコンポーネントを、使用可能な最も近い Hilt コンポーネント(この場合は
ApplicationComponent
)に移行し、必要に応じて null 可能性を使用する。
1 つ目は、すでに前のステップで、その状態になっています。このステップでは、2 つ目の方法でアプリケーションを Hilt に完全に移行します。なお、実際のアプリでは、個々のユースケースに合わせて選択して構いません。
このステップでは、UserComponent
が Hilt の ApplicationComponent
の一部となるように移行されます。このコンポーネントにモジュールがある場合は、そのモジュールも ApplicationComponent
にインストールする必要があります。
UserComponent
にあるスコープ設定されている型は、@LoggedUserScope
アノテーションが付いた UserDataRepository
のみです。UserComponent
は Hilt の ApplicationComponent
に集約するので、UserDataRepository
には @Singleton
アノテーションが付けられ、ユーザーがログアウトしたときに null になるようにロジックを変更します。
UserManager
はすでに @Singleton
アノテーションが付いているため、アプリ全体で同じインスタンスを提供でき、変更を加えれば、Hilt で同じ機能を実現できます。最初に下準備が必要になるので、まず UserManager
と UserDataRepository
の動作を変更します。
UserManager.kt
を開き、次のように変更してください。
UserComponent
インスタンスを作成する必要がなくなったため、コンストラクタ内のUserComponent.Factory
パラメータをUserDataRepository
で置き換えます。代わりに、依存関係としてUserDataRepository
があります。- Hilt がコンポーネント コードを生成するため、
UserComponent
とそのセッターを削除します。 userComponent
をチェックする代わりに、userRepository
からのユーザー名をチェックするようにisUserLoggedIn()
関数を変更します。userJustLoggedIn()
関数にユーザー名をパラメータとして追加します。userName
を引数としてuserDataRepository
のinitData
を呼び出すようにuserJustLoggedIn()
関数の本体を変更します。移行中に削除することになるuserComponent
の代わりです。registerUser()
関数とloginUser()
関数のuserJustLoggedIn()
呼び出しにusername
を追加します。logout()
関数からuserComponent
を削除し、userDataRepository.cleanUp()
への呼び出しに置き換えます。
以上の変更が終わると、UserManager.kt のコードは次のようになります。
UserManager.kt
@Singleton
class UserManager @Inject constructor(
private val storage: Storage,
// Since UserManager will be in charge of managing the UserComponent lifecycle,
// it needs to know how to create instances of it
private val userDataRepository: UserDataRepository
) {
val username: String
get() = storage.getString(REGISTERED_USER)
fun isUserLoggedIn() = userDataRepository.username != null
fun isUserRegistered() = storage.getString(REGISTERED_USER).isNotEmpty()
fun registerUser(username: String, password: String) {
storage.setString(REGISTERED_USER, username)
storage.setString("$username$PASSWORD_SUFFIX", password)
userJustLoggedIn(username)
}
fun loginUser(username: String, password: String): Boolean {
val registeredUser = this.username
if (registeredUser != username) return false
val registeredPassword = storage.getString("$username$PASSWORD_SUFFIX")
if (registeredPassword != password) return false
userJustLoggedIn(username)
return true
}
fun logout() {
userDataRepository.cleanUp()
}
fun unregister() {
val username = storage.getString(REGISTERED_USER)
storage.setString(REGISTERED_USER, "")
storage.setString("$username$PASSWORD_SUFFIX", "")
logout()
}
private fun userJustLoggedIn(username: String) {
// When the user logs in, we create populate data in UserComponent
userDataRepository.initData(username)
}
}
UserManager
に関する作業は完了しました。次に、UserDataRepository
にいくつか変更を行う必要があります。UserDataRepository.kt を開き、次のように変更してください。
@LoggedUserScope
の依存関係は Hilt が管理するようになるため、これを削除します。UserDataRepository
はすでにUserManager
に注入されているので、依存関係の循環を防ぐため、UserDataRepository
のコンストラクタからUserManager
パラメータを削除します。unreadNotifications
を null 許容に変更し、セッターを非公開にします。- 新しい null 許容変数
username
を追加し、セッターを非公開にします。 username
とunreadNotifications
を乱数に設定する新しい関数initData()
を追加します。username
とunreadNotifications
のカウントをリセットする新しい関数cleanUp()
を追加します。username
を null に、unreadNotifications
を -1 に設定します。- 最後に、クラス本体の
randomInt()
関数を移動します。
以上の変更が終わると、コードは次のようになります。
UserDataRepository.kt
@Singleton
class UserDataRepository @Inject constructor() {
var username: String? = null
private set
var unreadNotifications: Int? = null
private set
init {
unreadNotifications = randomInt()
}
fun refreshUnreadNotifications() {
unreadNotifications = randomInt()
}
fun initData(username: String) {
this.username = username
unreadNotifications = randomInt()
}
fun cleanUp() {
username = null
unreadNotifications = -1
}
private fun randomInt(): Int {
return Random.nextInt(until = 100)
}
}
UserComponent
の移行を完了するために、UserComponent.kt を開き、inject()
メソッドまでスクロールしてください。この依存関係は、MainActivity
と SettingsActivity
で使用されます。まず、MainActivity
から移行します。MainActivity.kt を開き、クラスに @AndroidEntryPoint
アノテーションを付けます。
MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
//...
}
UserManagerEntryPoint
インターフェースを削除し、onCreate()
からエントリ ポイント関連のコードも削除します。
MainActivity.kt
//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface UserManagerEntryPoint {
// fun userManager(): UserManager
//}
override fun onCreate(savedInstanceState: Bundle?) {
//val entryPoint = EntryPoints.get(applicationContext, UserManagerEntryPoint::class.java)
//val userManager = entryPoint.userManager()
super.onCreate(savedInstanceState)
//...
}
UserManager
に lateinit var
を宣言し、Hilt が依存関係を注入できるように @Inject
アノテーションを付けます。
MainActivity.kt
@Inject
lateinit var userManager: UserManager
UserManager
は Hilt によって注入されるため、UserComponent
に対する inject()
の呼び出しを削除します。
MainActivity.kt
//Remove
//userManager.userComponent!!.inject(this)
setupViews()
}
}
MainActivity
に必要な変更は以上です。同様の変更で、SettingsActivity
を移行できます。SettingsActivity
を開き、@AndroidEntryPoint
アノテーションを付けます。
SettingsActivity.kt
@AndroidEntryPoint
class SettingsActivity : AppCompatActivity() {
//...
}
UserManager
に lateinit var
を作成し、@Inject
アノテーションを付けます。
SettingsActivity.kt
@Inject
lateinit var userManager: UserManager
エントリ ポイント コードと、userComponent()
に対する注入呼び出しを削除します。これが終わると、onCreate()
関数は次のようになります。
SettingsActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
setupViews()
}
使用されないリソースをクリーンアップして、移行を完了します。LoggedUserScope.kt クラスと UserComponent.kt クラス、最後に AppSubcomponent.kt クラスを削除します。
再度、アプリを実行して試します。Dagger の場合と同じように動作するはずです。
Hilt へのアプリの移行を完了する前に、1 つの重要なステップが残っています。ここまでで、すべてのアプリコードを移行しましたが、テストは行っていません。Hilt は、アプリコードと同様に、テストにも依存関係を注入します。Hilt を使用したテストでは、テストごとに新しいコンポーネントのセットが自動的に生成されるため、メンテナンスは不要です。
単体テスト
単体テストを始めましょう。単体テストに Hilt を使用する必要はありません。コンストラクタにアノテーションがない場合と同様、対象のクラスのコンストラクタを直接呼び出して、疑似またはモックの依存関係を渡すことができるためです。
単体テストを実行すると、UserManagerTest でエラーになると思います。UserManager には、前のセクションで説明したコンストラクタのパラメータを含めて、多くの作業と変更を行っています。UserManagerTest.kt を開いてください。これはまだ UserComponent
と UserComponentFactory
に依存しています。UserManager
のパラメータはすでに変更されているので、UserComponent.Factory
パラメータを UserDataRepository
の新しいインスタンスに変更してください。
UserManagerTest.kt
@Before
fun setup() {
storage = FakeStorage()
userManager = UserManager(storage, UserDataRepository())
}
このように変更します。テストを再度実行すると、すべての単体テストが合格になるはずです。
テスト用の依存関係の追加
作業に入る前に、app/build.gradle
を開いて、次の Hilt 依存関係が存在することを確認してください。Hilt は、テスト専用のアノテーションのために hilt-android-testing
を使用します。また、Hilt は androidTest
フォルダにクラスのコードを生成する必要があるため、アノテーション プロセッサがそこで動作できる必要があります。
app/build.gradle
// Hilt testing dependencies
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
UI テスト
Hilt はテストごとに、テスト コンポーネントとテスト Application を自動的に生成します。まず、TestAppComponent.kt を開いて、移行の計画を行います。TestAppComponent
には、TestStorageModule
と AppSubcomponents
という 2 つのモジュールがあります。すでに AppSubcomponents
は移行して削除しているので、引き続き TestStorageModule
を移行します。
TestStorageModule.kt を開き、クラスに @InstallIn
アノテーションを付けてください。
TestStorageModule.kt
@InstallIn(ApplicationComponent::class)
@Module
abstract class TestStorageModule {
//...
すべてのモジュールの移行が完了したので、TestAppComponent
を削除します。
次に、Hilt を ApplicationTest
に追加しましょう。Hilt を使用する UI テストには @HiltAndroidTest
アノテーションを付ける必要があります。このアノテーションは、各テストに Hilt コンポーネントを生成させるものです。
ApplicationTest.kt を開き、次のアノテーションを追加してください。
@HiltAndroidTest
: このテスト用のコンポーネントを生成するよう Hilt に指示します。@UninstallModules(StorageModule::class)
: テスト中にTestStorageModule
が注入されるように、アプリコードで宣言されたStorageModule
をアンインストールするよう Hilt に指示します。HiltAndroidRule
をApplicationTest
に追加する必要もあります。このテストルールは、コンポーネントの状態を管理し、テストで注入を行うために使用されます。最終的なコードは次のようになります。
ApplicationTest.kt
@UninstallModules(StorageModule::class)
@HiltAndroidTest
class ApplicationTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
//...
Hilt は、各インストルメンテーション テストに対して新しい Application
を生成するため、UI テストの実行時に、Hilt が生成した Application
を使用するように指定する必要があります。これを行うには、カスタムのテストランナーが必要です。
Codelab アプリには、すでにカスタムのテストランナーがあります。MyCustomTestRunner.kt
を開きます。
Hilt には HiltTestApplication.
という名前のテストに使用できる Application
が用意されています。newApplication()
関数の本体で MyTestApplication::class.java
を HiltTestApplication::class.java
に変更する必要があります。
MyCustomTestRunner.kt
class MyCustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
この変更により、MyTestApplication.kt ファイルは削除できるようになりました。では、テストを実行しましょう。テストはすべて合格になるはずです。
Hilt には、WorkManager や ViewModel など、他の Jetpack ライブラリのクラスを提供するための拡張があります。Codelab プロジェクト内の ViewModel はプレーンなクラスなので、アーキテクチャ コンポーネントの ViewModel
を拡張していません。ViewModel の Hilt のサポートを追加する前に、アプリの ViewModel をアーキテクチャ コンポーネントの ViewModel に移行します。
ViewModel
と統合するには、Gradle ファイルに次の依存関係を追加する必要があります。これらの依存関係はすでに追加してあります。このライブラリとは別に、Hilt アノテーション プロセッサ上で動作する別のアノテーション プロセッサを追加する必要があります。
// app/build.gradle file
...
dependencies {
...
implementation "androidx.fragment:fragment-ktx:1.2.4"
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:$hilt_jetpack_version'
kapt 'androidx.hilt:hilt-compiler:$hilt_jetpack_version'
kaptAndroidTest 'androidx.hilt:hilt-compiler:$hilt_jetpack_version'
}
プレーンなクラスを ViewModel
に移行するには、ViewModel()
を拡張する必要があります。
MainViewModel.kt を開き、: ViewModel()
を追加します。アーキテクチャ コンポーネントの ViewModel に移行するにはこれで十分ですが、まだ ViewModel のインスタンスを提供する方法を Hilt に指示する必要があります。そのためには、ViewModel
のコンストラクタに @ViewModelInject
アノテーションを追加します。@Inject
アノテーションを @ViewModelInject
に置き換えてください。
MainViewModel.kt
class MainViewModel @ViewModelInject constructor(
private val userDataRepository: UserDataRepository
): ViewModel() {
//...
}
次に、LoginViewModel
を開いて同じ変更を加えます。最終的なコードは次のようになります。
LoginViewModel.kt
class LoginViewModel @ViewModelInject constructor(
private val userManager: UserManager
): ViewModel() {
//...
}
同様に、RegistrationViewModel.kt を開き、ViewModel()
に移行し、Hilt アノテーションを追加します。拡張メソッド viewModels()
と activityViewModels()
により ViewModel
のスコープを制御できるため、@ActivityScoped
アノテーションは必要ありません。
RegistrationViewModel.kt
class RegistrationViewModel @ViewModelInject constructor(
val userManager: UserManager
) : ViewModel() {
同じ変更で EnterDetailsViewModel
と SettingViewModel
を移行します。2 つのクラスの最終的なコードは次のようになります。
EnterDetailsViewModel.kt
class EnterDetailsViewModel @ViewModelInject constructor() : ViewModel() {
SettingViewModel.kt
class SettingsViewModel @ViewModelInject constructor(
private val userDataRepository: UserDataRepository,
private val userManager: UserManager
) : ViewModel() {
これで、すべての ViewModel がアーキテクチャ コンポーネントの ViewModel に移行され、Hilt アノテーションが付けられたので、その注入方法を移行できるようになりました。
次に、View レイヤで ViewModel を初期化する方法を変更する必要があります。ViewModel は OS によって作成され、by viewModels()
委譲関数を使用して取得できます。
MainActivity.kt を開き、@Inject
アノテーションを Jetpack 拡張に置き換えます。また、lateinit
を削除し、var
を val
に変更し、そのフィールドを private
とマークする必要があります。
MainActivity.kt
// @Inject
// lateinit var mainViewModel: MainViewModel
private val mainViewModel: MainViewModel by viewModels()
同じように、LoginActivity.kt を開き、ViewModel
の取得方法を変更します。
LoginActivity.kt
// @Inject
// lateinit var loginViewModel: LoginViewModel
private val loginViewModel: LoginViewModel by viewModels()
次に、RegistrationActivity.kt を開き、同じように registrationViewModel
の取得方法を変更します。
RegistrationActivity.kt
// @Inject
// lateinit var registrationViewModel: RegistrationViewModel
private val registrationViewModel: RegistrationViewModel by viewModels()
EnterDetailsFragment.kt を開きます。EnterDetailsViewModel
の取得方法を置き換えます。
EnterDetailsFragment.kt
private val enterDetailsViewModel: EnterDetailsViewModel by viewModels()
同様に、registrationViewModel
の取得方法を変更しますが、今回は viewModels().
ではなく activityViewModels()
委譲関数を使用します。registrationViewModel
が注入されると、Hilt はアクティビティ レベルでスコープ設定された ViewModel を注入します。
EnterDetailsFragment.kt
private val registrationViewModel: RegistrationViewModel by activityViewModels()
TermsAndConditionsFragment.kt を開き、再び viewModels()
ではなく activityViewModels()
拡張関数を使用して registrationViewModel.
を取得するようにします。
TermsAndConditionsFragment.kt
private val registrationViewModel: RegistrationViewModel by activityViewModels()
最後に、SettingsActivity.kt を開き、settingsViewModel
の取得方法を移行します。
SettingsActivity.kt
private val settingsViewModel: SettingsViewModel by viewModels()
アプリを実行して、すべてが期待どおりに動作することを確認します。
これで、アプリが Hilt を使用するようにする移行が完了しました。単に移行を完了しただけでなく、Dagger コンポーネントを 1 つずつ移行しながら、アプリケーションの動作を維持することもできました。
この Codelab では、Application コンポーネントから始めて、Hilt を既存の Dagger コンポーネントと共存させるために必要な土台を構築しました。そこから、Activity と Fragment に Hilt アノテーションを使用し、Dagger 関連のコードを削除することで、各 Dagger コンポーネントを Hilt へ移行しました。1 つのコンポーネントの移行を完了するごとに、アプリが動作し正しく機能することが確認できました。また、Context
依存関係と ApplicationContext
依存関係を、Hilt が提供する @ActivityContext
アノテーションと @ApplicationContext
アノテーションを使用して移行しました。他の Android コンポーネントも移行しました。最後に、テストを移行して、Hilt への移行を完了しました。
参考資料
アプリを Hilt に移行する方法について詳しくは、Hilt への移行をご覧ください。Dagger から Hilt への移行の詳細だけでなく、dagger.android アプリの移行に関する情報もご覧ください。