【Codelab】Migrating your Dagger app to Hiltメモ
1. Introduction
依存性注入(DI)は、コードの再利用性、リファクタリングの容易さ、テストの容易さを実現するのに役立ちます。Hiltは、人気の高いDIライブラリDaggerの上に構築されており、Daggerが提供するコンパイル時の正確性、実行時の性能、スケーラビリティ、Android Studioのサポートの恩恵を受けることができます。 Android Studioのサポート
先日リリースされたAndroid Studio 4.1で追加されています。
Androidのフレームワーククラスの多くはOS自体がインスタンス化しているため、AndroidアプリでDaggerを使用する際にはそれに関連したボイラプレートが存在します。Hiltは、以下を自動生成して提供することで、このボイラプレートの大部分を取り除きます。
手作業で作成しなければならないようなAndroidフレームワークのクラスをDaggerで統合するためのコンポーネント
Hiltが自動的に生成するコンポーネントのScope Annnotation
定義済みのbindingとqualifiers
何より、DaggerとHiltが共存できるので、必要に応じてアプリの移行が可能です。
Prerequisites
Experience with Kotlin syntax.
Experience with Dagger.
What you'll learn
How to add Hilt to your Android app.
How to plan your migration strategy.
How to migrate components to Hilt and keep the existing Dagger code working.
How to migrate scoped components.
How to test your app using Hilt.
What you'll need
Android Studio 4.0 or higher.
2. Getting set up
Get the code
$ git clone https://github.com/googlecodelabs/android-dagger-to-hilt
Open Android Studio
vcsからopenすると簡単
Project set up
このプロジェクトは複数のブランチで構成されています。
master はチェックアウトしたりダウンロードした際のデフォルトのブランチです。コードラボの出発点です。
interop はDaggerとHiltが共存するブランチです。
solution はtestとViewModelを含むコードラボのソリューションが含まれるブランチです。
masterブランチから始めて、自分のペースでコーデラボのステップを踏んでいくことをお勧めします。コードラボでは、プロジェクトに追加しなければならないコードのスニペットが提示されます。いくつかの場所では、コードスニペットのコメントで明示的に言及されているコードを削除しなければならないこともあります。
Running the sample app
まずは、起動するサンプルアプリがどのようなものか見てみましょう。
https://gyazo.com/3e15cac919595beaa51591565acf6f42
このアプリは、Daggerを使った4つの異なるフローで構成されています(アクティビティとして実装されています)。
Registration: ユーザーは、ユーザー名、パスワードを入力し、規約に同意することで登録することができます。
Login: 登録フローで追加されたcredentialsを使ってログインしたり、アプリからの登録解除も可能です。
Home: ユーザーは未読通知の数を確認することができます。
Settings: ユーザーはログアウトして、未読通知の数を更新することができます(未読通知の数が乱数になります)。
プロジェクトは典型的な MVVM パターンに沿っており、複雑なViewのすべての処理はViewModelへと延期されます。プロジェクトの構造を理解するために、少し時間を取ってみてください。
https://gyazo.com/b131216164923529a3af6bcd5b156eb4
矢印はオブジェクト間の依存関係を表します。これがアプリケーショングラフと呼ばれるもので、アプリのすべてのクラスとその間の依存関係です。
masterブランチのコードはDaggerを使って依存関係を注入しています。手作業でコンポーネントを作成するのではなく、アプリをリファクタリングして、Hiltを使用してコンポーネントやその他のDagger関連のコードを生成するようにします。
Daggerは以下の図のようにアプリ内で設定されています。特定のタイプにドットが付いているのは、そのタイプが提供するComponentにスコープされていることを意味します。
https://gyazo.com/24814030a4cdd67a8eb094288dd7985d
3. Adding Hilt to the project
物事をシンプルにするために、Hiltの依存関係は、最初にダウンロードしたmasterブランチですでにこのプロジェクトに追加されています。以下のコードをプロジェクトに追加する必要はありません。とはいえ、Android アプリでHiltを使うために必要なものを見てみましょう。
ライブラリの依存関係とは別に、Hiltはプロジェクトで設定されたGradleプラグインを使用します。ルート (プロジェクトレベル) の build.gradleファイルを開き、classpathに以下のHilt依存関係を見つけます。
code:gradle
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プラグイン宣言を確認します。
code:app/build.gradle
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
}
最後に、Hiltの依存関係とアノテーションプロセッサは、同じapp/build.gradleファイルに含まれています。
code:app/build.gradle
...
dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}
プロジェクトをビルドして同期すると、Hiltを含むすべてのライブラリがダウンロードされます。Hilt を使い始めましょう!
プロジェクトのビルド中に問題が発生した場合は、クリーンを選択して Dagger や Hilt で生成されたコードを削除し、プロジェクトを再度ビルドしてください。
4. Planning the migration
一度にすべてをHiltに移行したくなるかもしれませんが、現実のプロジェクトでは、段階的にHiltに移行している間に、アプリをビルドしてエラーなく実行したいと思うでしょう。
Hiltに移行する際には、作業を段階的に行いたいと思うでしょう。推奨されるアプローチは、まずApplicationまたは@Singleton コンポーネントの移行から始め、その後でアクティビティやフラグメントの移行を行うことです。
コードラボでは、最初にAppComponentを移行し、登録から始まり、ログイン、そして最後にメインと設定というアプリの各フローを移行していきます。
移行の際には、@Componentと@Subcomponentのインターフェイスをすべて削除し、すべてのモジュールに@InstallInをアノテーションします。
移行後、すべてのApplication/Activity/Flagment/View/Service/BroadcastReceiverクラスに @AndroidEntryPointのアノテーションを付け、コンポーネントのインスタンス化やプロパゲーションを行うコードも削除する必要があります。
移行計画を立てるために、まずはAppComponent.ktからコンポーネントの階層を理解しましょう。
code:AppComponent.kt
@Singleton
// Definition of a Dagger component that adds info from the different modules to the graph
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へinjectされます。
RegistrationComponentはRegistrationActivity、EnterDetailsFragment、TermsAndConditionsFragmentにinjectされます。そして、このComponentはRegistrationActivityにスコープされます。
UserComponentはMainActivityとSettingsActivityに注入されています。ApplicationComponentへの参照は、アプリ内で移行するComponentにマップされるHilt生成されたComponent (生成されたすべてのコンポーネントへのリンク) で置き換えることができます。
5. Migrating the Application component
このセクションでは、AppComponentを移行します。以下の手順で各コンポーネントをHiltに移行しながら、既存のDaggerコードを動作させるための基礎作業を行う必要があります。
Hiltを初期化してコード生成を開始するには、ApplicationクラスにHiltアノテーションを付ける必要があります。
MyApplication.ktを開き、@HiltAndroidAppアノテーションをクラスに追加します。これらのアノテーションは、Daggerがアノテーションプロセッサで使用するコードの生成をトリガーするようにHiltに指示します。
code:MyApplicaion.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. Migrate Component modules
はじめに、AppComponent.ktを開きます。AppComponentには、@Componentアノテーションで追加された2つのモジュール (StorageModuleとAppSubcomponents) があります。まずはこの2つのモジュールを移行して、Hiltが生成したApplicationComponentに追加するようにします。
そのためには、AppSubcomponents.ktを開き、クラスに@InstallInアノテーションを付けます。@InstallInのアノテーションは、適切なコンポーネントにモジュールを追加するためのパラメータを取ります。この場合、アプリケーションレベルのコンポーネントを移行しているので、バインディングはApplicationComponentで生成したいと思います。
code:AppSubComponent.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アノテーションを追加します。
code: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. Migrate exposed types
アプリを完全にHiltに移行している間、Hiltではentry pointsを使用してDaggerから依存関係を手動で要求することができます。entry pointsを使用することで、Daggerコンポーネントをすべて移行しながらアプリを動作させることができます。このステップでは、各Daggerコンポーネントを、Hiltによって生成されたApplicationComponentの依存関係を手動で見つけることで置き換えます。 Hiltが生成したApplicationComponentからRegistrationActivity.ktのRegistrationComponent.Factoryを取得するには、@InstallInでアノテーションされた新しいEntryPointインターフェイスを作成する必要があります。@InstallInのアノテーションは、バインディングをどこから取得するかをHiltに指示します。EntryPointにアクセスするには、EntryPointAccessorsから適切な静的メソッドを使用します。パラメータは、コンポーネントインスタンスまたはコンポーネントホルダーとして動作する@AndroidEntryPointオブジェクトのいずれかである必要があります。
code:RegistrationActivity.kt
class RegistrationActivity : AppCompatActivity() {
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface RegistrationEntryPoint {
fun registrationComponent(): RegistrationComponent.Factory
}
...
}
あとはDagger関連のコードをRegistrationEntryPointに置き換える必要があります。registrationComponentの初期化を変更して、RegistrationEntryPointを使用するようにします。この変更により、RegistrationActivityは、Hiltを使用するように移行されるまでの間、Hiltで生成されたコード上の依存関係にアクセスすることができます。
code: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()
次に、公開されている他のすべてのタイプのコンポーネントについて、同じ基礎作業を行う必要があります。LoginComponent.Factoryの続きをしてみましょう。LoginActivityを開き、先ほどと同様に@InstallInと@EntryPoint でアノテーションされたLoginEntryPointインターフェイスを作成しますが、LoginActivityがHilt コンポーネントから必要とするものを公開します。
code:LoginActivity.kt
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface LoginEntryPoint {
fun loginComponent(): LoginComponent.Factory
}
これで、HiltがLoginComponentを提供する方法を知ったので、古いinject()コールをEntryPointのloginComponent() で置き換えます。
code:LoginActivity.kt
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, LoginEntryPoint::class.java)
entryPoint.loginComponent().create().inject(this)
AppComponentから公開されている3つのタイプのうち2つは、Hilt EntryPointsで動作するように置き換えられています。次に、UserManagerについても同様の変更を行う必要があります。UserManagerは、RegistrationComponentと LoginComponentとは異なり、MainActivityとSettingsActivityの両方で使用されます。EntryPointインターフェースを作成する必要があるのは1回だけです。注釈付きのEntryPointインターフェイスは、両方のアクティビティで使用できます。シンプルにするために、MainActivityでインターフェイスを宣言します。
EntryPointは通常、使用するクラスで宣言されます。EntryPointが複数のクラスで使用されている場合は、新しいクラスで宣言し、utilなどの共通のパッケージに配置することができます。
UserManagerEntryPointインターフェースを作成するには、MainActivity.ktを開き、@InstallInと@EntryPointで注釈を付けます。
code:MainActivity.kt
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface UserManagerEntryPoint {
fun userManager(): UserManager
}
ここで、UserManagerを変更して、UserManagerEntryPointを使用するようにします。
code:MainActivity.kt
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, UserManagerEntryPoint::class.java)
val userManager = entryPoint.userManager()
SettingsActivityでも同じように変更する必要があります。SettingsActivity.ktを開き、UserManagerの注入方法を入れ替えます。
code:SettingsActivity.kt
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, MainActivity.UserManagerEntryPoint::class.java)
val userManager = entryPoint.userManager()
3. Remove Component Factory
Daggerコンポーネントに@BindsInstanceを使ってContextを渡すのはよくあるパターンです。Hiltでは、Contextは既に定義済みのバインディングとして利用可能なので、これは必要ありません。
コンテキストは通常、リソースやデータベース、共有環境設定などにアクセスするために必要です。Hiltは、修飾子 @ApplicationContextと@ActivityContextを使用してコンテキストへの注入を簡素化します。
アプリを移行する際には、依存関係としてContextを必要とするタイプを確認し、Hiltが提供するものに置き換えてください。
この場合、SharedPreferencesStorageはContextを依存関係として持っています。HiltにContextを注入するように指示するには、SharedPreferencesStorage.ktを開きます。SharedPreferencesはアプリケーションのContextを必要とするので、contextパラメータに@ApplicationContextアノテーションを追加します。
code:SharedPreferencesStorage.kt
class SharedPreferencesStorage @Inject constructor(
@ApplicationContext context: Context
) : Storage {
//...
4. Migrate inject methods
次に、コンポーネントのコードにinject()メソッドがあるかどうかをチェックし、対応するクラスに@AndroidEntryPointをアノテーションする必要があります。私たちの場合、AppComponentにはinject()メソッドがないので、何もする必要はありません。
5. Remove the AppComponent class
AppComponent.ktに記載されているすべてのコンポーネントのEntryPointsをすでに追加しているので、AppComponent.ktを削除します。
6. Remove the code that uses the Component to migrate
アプリケーションクラスのカスタムAppComponentを初期化するコードはもう必要ありません。クラス本体内のコードをすべて削除します。最終的なコードは、以下のコード一覧のようになるはずです。
code:MyApplication.kt
package com.example.android.dagger
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
open class MyApplication : Application()
これで、アプリにHiltを追加し、AppComponentを削除し、Daggerコードを変更して、Hiltによって生成されたAppComponentの上に依存関係を注入することができました。アプリをビルドしてデバイスやエミュレータで試してみると、アプリは以前と同じように動作しているはずです。次ののセクションでは、 ActivityとFragment をHiltを使用するように移行していきます。
6. Migrating an Activity component
これで、Applicationコンポーネントの移行が完了し、下地ができたので、各コンポーネントを一つずつHiltに移行していきます。
ログインフローの移行を始めましょう。LoginComponentを手動で作成してLoginActivityで使用するのではなく、Hiltを使用します。
前のセクションで使用したのと同じ手順に従うことができますが、今回はアクティビティによって管理されているコンポーネントを移行するため、Hiltで生成されたActivityComponentを使用します。
まずはLoginComponent.ktを開きます。LoginComponentにはモジュールがないので何もする必要はありません。HiltにLoginActivity用のコンポーネントを生成させて注入するには、@AndroidEntryPointでアクティビティにアノテーションをつける必要があります。
code:LoginActivity.kt
@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {
//...
}
これがLoginActivityをHiltに移行するために追加するコードです。HiltはDagger関連のコードを生成してくれるので、必要なのは少しのクリーンアップだけです。LoginEntryPointインターフェイスを削除します。
code:LoginActivity.kt
//Remove
//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface LoginEntryPoint {
// fun loginComponent(): LoginComponent.Factory
//}
次に、onCreate()内のEntryPointコードを削除します。
code: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を削除しても問題ありません。
code: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を使用すると、入力するコードが減るだけでなく、メンテナンスのためのコードが減り、バグが発生しにくくなります。
7. Migrating an Activity and Fragment components
このセクションでは、登録フローを移行します。移行を計画するために、RegistrationComponentを見てみましょう。RegistrationComponent.ktを開き、inject()関数までスクロールダウンします。RegistrationComponentは、RegistrationActivity、EnterDetailsFragment、TermsAndConditionsFragmentへの依存関係の注入を担当します。
まずはRegistrationActivityの移行から始めてみましょう。RegistrationActivity.ktを開き、クラスに@AndroidEntryPointをアノテーションします。
code:RegistrationActivity.kt
@AndroidEntryPoint
class RegistrationActivity : AppCompatActivity() {
//...
}
これでRegistrationActivityがHiltに登録されたので、onCreate()関数からRegistrationEntryPointインターフェイスとEntryPoint関連のコードを削除します。
code: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はコンポーネントの生成と依存関係の注入を担当しているので、registerComponent変数と削除されたDaggerコンポーネントのinjectコールを削除することができます。
code:RegistrationActivity.kt
// Remove
// lateinit var registrationComponent: RegistrationComponent
override fun onCreate(savedInstanceState: Bundle?) {
//Remove
//registrationComponent.inject(this)
super.onCreate(savedInstanceState)
//..
}
次に、EnterDetailsFragment.ktを開きます。RegistrationActivityで行ったのと同様に、EnterDetailsFragmentに@AndroidEntryPointで注釈を付けます。
code:EnterDetailsFragment.kt
@AndroidEntryPoint
class EnterDetailsFragment : Fragment() {
//...
}
Hiltが依存関係を提供しているので、削除されたDaggerコンポーネントに対するinject()呼び出しは不要です。onAttach() 関数を削除します。
次のステップでは、TermsAndConditionsFragmentを移行します。TermsAndConditionsFragment.ktを開き、クラスに注釈を付け、前のステップで行ったようにonAttach()関数を削除します。最終的なコードは以下のようになります。
code: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のサブコンポーネントリストからその参照を削除する必要があります。
code:AppSubcomponents.kt
@InstallIn(ApplicationComponent::class)
// This module tells a Component which are its subcomponents
@Module(
subcomponents = [
UserComponent::class
]
)
class AppSubcomponents
登録フローの移行を終わらせるには、あと1つ残っています。登録フローは、ActivityScopeという独自のスコープを宣言して使用します。スコープは依存関係のライフサイクルを制御します。この場合、ActivityScopeはRegistrationActivityで開始したフロー内にRegistrationViewModelの同じインスタンスを注入するようにDaggerに指示します。Hilt はこれをサポートするためにビルトインされたライフサイクルスコープを提供しています。
RegistrationViewModelを開き@ActivityScopeアノテーションをHiltが提供する@ActivityScopedに変更します
code:RegistrationViewModel.kt
@ActivityScoped
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
//...
}
ActivityScopeはどこにも使われていないので、安全に削除することができます。
それではアプリを起動して、登録フローを試してみましょう。現在のユーザー名とパスワードを使ってログインしたり、新しいアカウントで登録を解除して再登録したりして、フローが以前と同じように動作することを確認することができます。
現在、アプリ内ではDaggerとHiltが連携しています。HiltはUserManager以外の依存関係を全て注入しています。次のセクションでは、UserManagerを移行してDaggerからHiltに完全移行します。
8. Migrating another scoped component
これまでのところ、このコードラボでは、UserComponentを除いて、サンプルアプリのほとんどをHiltに移行することに成功しています。UserComponentには@LoggedUserScopeというカスタム スコープがアノテーションされています。つまり、UserComponentは@LoggedUserScopeでアノテーションされたクラスにUserManagerの同じインスタンスを注入します。
UserComponentは、そのライフサイクルがAndroidクラスによって管理されていないため、利用可能なHiltコンポーネントにマッピングされません。生成されたhilt階層の途中にカスタムコンポーネントを追加することはサポートされていないので、2つの選択肢があります。
1. HiltとDaggerを並べた状態で放置しておきます。
2. スコープされたコンポーネントを最も近い利用可能なHiltコンポーネント (この場合はApplicationComponent) に移行し、必要に応じて nullability を使用します。
前のステップではすでに1つ目の選択肢を達成しています。このステップでは、アプリケーションをHiltに完全に移行するために、2つ目の選択肢をとります。しかし、実際のアプリでは、特定のユースケースに適したものを自由に選択することができます。
このステップでは、UserComponentがHiltのApplicationComponentの一部になるように移行されます。そのコンポーネントにモジュールがある場合は、それらも同様にApplicationComponentにインストールする必要があります。
UserComponentでスコープされた型はUserDataRepositoryだけです 。これは@LoggedUserScopeでアノテーションされています。UserComponentはHiltのApplicationComponentに収束するので、UserDataRepositoryは@Singletonでアノテーションされ、ユーザーがログアウトしたときに null になるようにロジックを変更します。
UserManagerは既に@Singletonでアノテーションされており、アプリ全体で同じインスタンスを提供できることを意味します。まずは、UserManagerとUserDataRepositoryの動作方法を変更することから始めましょう。
UserManager.ktを開き、以下の変更を適用します。
UserComponentのインスタンスを作成する必要がなくなったので、コンストラクタでUserComponent.FactoryパラメータをUserDataRepositoryに置き換えてください。
Hiltはコンポーネントコードを生成するので、UserComponentとそのセッターを削除します。
isUserLoggedIn()関数を変更し、userComponentをチェックする代わりにuserRepositoryからユーザー名をチェックするようにしました。
userJustLoggedIn()関数のパラメータとしてユーザ名を追加します。
userJustLoggedIn()関数の本体を変更し、移行中に削除されるuserComponentの代わりにinitDataをuserDataRepositoryでuserNameを指定して呼び出すようにしました。
registerUser()およびloginUser()関数のuserJustLoggedIn()コールにユーザ名を追加。
logout()関数からuserComponentを削除し、userDataRepository.initData(username)の呼び出しに置き換えます。
UserManager.ktの最終的なコードは以下のようになります。
code: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を開き、以下の変更を適用します。
依存関係はHiltが管理するので、@LoggedUserScopeを削除します。
UserDataRepositoryはすでにUserManagerに注入されているので、循環的な依存関係を避けるために、UserDataRepositoryのコンストラクタからUserManagerパラメータを削除してください。
unreadNotificationsをnullableに変更し、セッターをprivateにします。.
新しいnullableな変数usernameを追加し、セッターをprivateにします。
usernameとunreadNotificationsを乱数に設定する関数initData()を追加します。
usernameとunreadNotificationsをリセットするために、新しい関数cleanUp()を追加します。usernameをnullに、unreadNotificationsを-1に設定します。
最後に、randomInt()関数をクラス本体の中に移動させます。
最終的なコードは以下のようになるはずです。
code: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をアノテーションします。
code:MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
//...
}
UserManagerEntryPointインターフェイスを削除し、エントリポイント関連のコードをonCreate()から削除します。
code: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アノテーションを付けます。
code:MainActivity.kt
@Inject
lateinit var userManager: UserManager
UserManagerは Hilt によって注入されるので、UserComponentのinject()コールを削除します。
code:MainActivity.kt
//Remove
//userManager.userComponent!!.inject(this)
setupViews()
}
}
MainActivityに対して行う必要があるのはこれだけです。あとは、SettingsActivityを移行するために同様の変更を行います。SettingsActivityを開き、@AndroidEntryPointで注釈を付けます。
code:SettingsActivity.kt
@AndroidEntryPoint
class SettingsActivity : AppCompatActivity() {
//...
}
UserManager用の変数をlateinit var で用意し、@Injectアノテーションを付けます。
code:SettingsActivity.kt
@Inject
lateinit var userManager: UserManager
エントリーポイントのコードとuserComponent()のinjectコールを削除します。これが終わったら、onCreate()関数は以下のようになるはずです。
code:SettingsActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
setupViews()
}
これで未使用のリソースをクリーンアップして移行を完了させます。LoggedUserScope.kt、UserComponent.kt、最後にAppSubcomponent.ktクラスを削除します。
ここで、アプリを実行してもう一度試してみてください。アプリはDaggerで使用していたのと同じように機能しているはずです。
9. Testing
アプリのHiltへの移行を完了する前に、1つの重要なステップが残っています。ここまでですべてのコードを移行しましたが、テストは移行していません。Hiltはアプリのコードと同じようにテストに依存関係を注入します。Hiltを使用したテストでは、テストごとに新しいコンポーネントのセットが自動的に生成されるため、メンテナンスは必要ありません。
Unit tests
ユニットテストから始めましょう。コンストラクタがアノテーションされていない場合と同じように、ターゲットクラスのコンストラクタを直接呼び出すことができます。
ユニットテストを実行すると、UserManagerTestが失敗していることがわかります。前のセクションでコンストラクタのパラメータを含めて、UserManagerに多くの作業と変更を行いました。UserComponentとUserComponentFactoryに依存しているUserManagerTest.ktを開きます。すでにUserManagerのパラメータを変更しているので、UserComponent.FactoryパラメータをUserDataRepositoryの新しいインスタンスで変更します。
code:UserManagerTest.kt
@Before
fun setup() {
storage = FakeStorage()
userManager = UserManager(storage, UserDataRepository())
}
これです! テストをもう一度実行すると、すべてのユニットテストが通過しているはずです。
Adding Testing Dependencies
app/build.gradleを開き、以下のHiltの依存関係が存在することを確認してください。Hiltはテスト固有のアノテーションに hilt-android-testingを使用します。さらに、HiltはandroidTest フォルダ内のクラスのコードを生成する必要があるため、アノテーション プロセッサもそこで実行できる必要があります。
code: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 Tests
Hiltは、テストごとにテストコンポーネントとテストアプリケーションを自動的に生成します。開始するには、TestAppComponent.ktを開いて移行計画を立てます。TestAppComponentには、TestStorageModuleとAppSubcomponentsの2つのモジュールがあります。AppSubcomponentsは既に移行して削除しているので、TestStorageModuleの移行を続けます。
TestStorageModule.ktを開き、@InstallInのアノテーションを付けます。
code:TestStorageModule.kt
@InstallIn(ApplicationComponent::class)
@Module
abstract class TestStorageModule {
//...
全てのモジュールの移行が終わったので、TestAppComponentを削除します。
次にApplicationTestにHiltを追加してみましょう。Hiltを使用するUIテストには@HiltAndroidTestというアノテーションを付ける必要があります。このアノテーションは、各テストのHiltコンポーネントを生成する役割を担っています。
ApplicationTest.ktを開き、以下のアノテーションを追加します。
@HiltAndroidTestは、このテスト用のコンポーネントを生成するように Hilt に指示します。
@UninstallModules(StorageModule::class)を使用して、テスト中にTestStorageModuleが代わりに注入されるように、アプリコードで宣言されたStorageModuleをアンインストールするようにHiltに指示します。
また、ApplicationTestにHiltAndroidRuleを追加する必要があります。このテストルールはコンポーネントの状態を管理し、テストでインジェクションを実行するために使用します。最終的なコードは以下のようになるはずです。
code:ApplicationTest.kt
@UninstallModules(StorageModule::class)
@HiltAndroidTest
class ApplicationTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
//...
Hiltはインスツルメンテーションテストのたびに新しいアプリケーションを生成するため、UIテストを実行する際には、Hiltが生成したアプリケーションを使用するように指定する必要があります。これを行うには、カスタムテストランナーが必要です。
codelabアプリには、すでにカスタムテストランナーがあります。MyCustomTestRunner.ktを開きます。
Hiltには、HiltTestApplicationという名前のテストに使用できるアプリケーションが既に用意されています。MyTestApplication::class.javaをHiltTestApplication::class.javaに変更する必要があります。
code: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ファイルを削除しても安全です。さあ、先に進んでテストを実行してください。全てのテストが通過しているはずです。
10. Optional Migrate ViewModels
Hiltには、WorkManagerやViewModelなどの他のJetpackライブラリからクラスを提供するための拡張機能が含まれています。codelabプロジェクトのViewModelsは、Architecture ComponentsのViewModelを継承していないプレーンなクラスです。Hiltの ViewModelsサポートを追加する前に、アプリ内のViewModelsをArchitecture Componentsのものに移行してみましょう。 ViewModelと統合するには、以下の依存関係をgradleファイルに追加する必要があります。これらの依存関係は既に追加されています。ライブラリとは別に、Hiltアノテーションプロセッサの上で動作するアノテーションプロセッサを追加する必要があることに注意してください。
code:app/build.gradle
// 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()を追加します。これだけでArchitecture ComponentsのViewModelsに移行できますが、ViewModelのインスタンスを提供する方法をHiltに伝える必要があります。そのためには、ViewModelのコンストラクタに@ViewModelInjectアノテーションを追加します。@Injectアノテーションを @ViewModelInjectに置き換えます。
code:MainViewModel.kt
class MainViewModel @ViewModelInject constructor(
private val userDataRepository: UserDataRepository
): ViewModel() {
//...
}
次に、LoginViewModelを開き、同じ変更を行います。最終的なコードは以下のようになります。
code:LoginViewModel.kt
class LoginViewModel @ViewModelInject constructor(
private val userManager: UserManager
): ViewModel() {
//...
}
同様にRegistrationViewModel.ktを開いて ViewModel()に移行し、Hiltアノテーションを追加します。拡張メソッド viewModels()とactivityViewModels()でViewModelのスコープを制御できるので、@ActivityScopedアノテーションは必要ありません。
code:RegistrationViewModel.kt
class RegistrationViewModel @ViewModelInject constructor(
val userManager: UserManager
) : ViewModel() {
EnterDetailsViewModelとSettingViewModelを移行するためにも同じ変更を行います。これら2つのクラスの最終的なは以下のようになるはずです。
code:EnterDetailsViewModel.kt
class EnterDetailsViewModel @ViewModelInject constructor() : ViewModel() {
code:SettingViewModel.kt
class SettingsViewModel @ViewModelInject constructor(
private val userDataRepository: UserDataRepository,
private val userManager: UserManager
) : ViewModel() {
これで、すべてのViewModelsがArchitecture Component Viewmodelsに移行され、Hiltアノテーションでアノテーションされたので、どのように注入されるかを移行することができます。
次に、ViewレイヤーでのViewModelsの初期化方法を変更する必要があります。ViewModelsはOS によって作成され、その取得方法はby viewModels()デリゲート関数を使用します。
MainActivity.ktを開き、@InjectアノテーションをJetpackの拡張関数に置き換えます。また、lateinitを削除し、varをvalに変更し、フィールドをprivateにする必要があることに注意してください。
code:MainActivity.kt
// @Inject
// lateinit var mainViewModel: MainViewModel
private val mainViewModel: MainViewModel by viewModels()
同様に、LoginActivity.ktを開き、ViewModelの取得方法を変更します。
code:LoginActivity.kt
// @Inject
// lateinit var loginViewModel: LoginViewModel
private val loginViewModel: LoginViewModel by viewModels()
次に、RegistrationActivity.ktを開き、同様の変更を適用してregistrationViewModelを取得します。
code:RegistrationActivity.kt
// @Inject
// lateinit var registrationViewModel: RegistrationViewModel
private val registrationViewModel: RegistrationViewModel by viewModels()
EnterDetailsFragment.ktを開きます。EnterDetailsViewModelの取得方法を置き換えます。
code:EnterDetailsFragment.kt
private val enterDetailsViewModel: EnterDetailsViewModel by viewModels()
同様にregistrationViewModelの取得方法を置き換えますが、今回はviewModels()の代わりにactivityViewModels()デリゲート関数を使用します。registrationViewModelが注入されると、HiltはアクティビティレベルでスコープされたViewModelを注入します。
code:EnterDetailsFragment.kt
private val registrationViewModel: RegistrationViewModel by activityViewModels()
TermsAndConditionsFragment.ktを開き、再度、viewModels()の代わりにactivityViewModels()の拡張関数を使用してregistrationViewModelを取得します。
code:TermsAndConditionsFragment.kt
private val registrationViewModel: RegistrationViewModel by activityViewModels()
最後に、SettingsActivity.ktを開き、settingsViewModelの取得方法を移行します。
code:SettingsActivity.kt
private val settingsViewModel: SettingsViewModel by viewModels()
今すぐアプリを実行して、すべてが期待通りに動作することを確認してください。
11. Congratulations!
おめでとうございます。Hiltを使用するアプリの移行に成功しましたね。移行が完了しただけでなく、Daggerコンポーネントを1 つずつ移行しながらアプリケーションの動作を維持しました。
このコードラボでは、Application Componentから始めて、既存のDaggerコンポーネントでHiltを動作させるために必要な基盤を構築する方法を学びました。その後、ActivityとFragmentのHiltアノテーションを使用して、Dagger関連のコードを削除して、各DaggerコンポーネントをHiltに移行しました。コンポーネントの移行が完了するたびに、アプリは期待通りに動作し、機能しました。また、Hiltが提供する@ActivityContextと@ApplicationContextアノテーションを使用して、ContextとApplicationContextの依存関係を移行しました。他のAndroid コンポーネントも移行しました。最後にテストを移行して、Hiltへの移行を終了します。
Further reading
アプリのHiltへの移行についての詳細は、Migrating to Hiltのドキュメントをチェックしてください。DaggerからHiltへの移行に関する詳細情報の他に、dagger.androidアプリの移行に関する情報もあります。