こんにちは。Android アプリ開発担当の nagayama(@nagayan_dev)です。
今回私が担当している「auサービスToday」では、これまで Jetpack Compose ・マルチモジュール・ Coroutines を導入してきました。今回は Hilt を導入したため、その対応内容と結果についてお話しできたらと思います。
- Jetpack Compose 最速導入フローチャート - Gunosy Tech Blog
- Androidのマルチモジュール化対応とその効果 - Gunosy Tech Blog
- Android の Kotlin Coroutines 導入の第一歩 - Gunosy Tech Blog
「auサービスToday」はすでに Dagger を導入済みであるため、Dagger を Hilt にバージョンアップしていく方針となります。
Hilt とは
Hilt は Android 用の依存関係インジェクションライブラリで、Dagger の上に構築されています。Dagger は今まで分かりづらく、学習コストが高いと言われてきました。Hilt はそれを解決すべく作成されたライブラリになります。
Hilt を日本語にすると刀の「柄」になります。Dagger が「短剣」に対してこの名前が付けられたと ChatGPT 先生が教えてくれました。
Dagger と Hilt の比較
Dagger から Hilt にするメリットは下記の 2 点にあると思います。
① 簡単に実装できる
そもそも Hilt は Dagger の上に構築されたライブラリで、Dagger をより簡単に実装できるようになります。Android と一緒に開発されているため Jetpack ライブラリと親和性があり、ViewModel や Compose 等での利用が容易になります。
② テストコードが簡単に書ける
Dagger でのテストはボイラープレートと Dagger の設定に手間がかかります。ですが Hilt ではテスト用のアノテーションを使用するだけで、依存性の注入をテストすることができます。今後「auサービスToday」でのテストコードを拡張していこうと考えていたため、今回を機に Hilt でテストコードが簡単に作成できればと思いました。
対応内容
Android Developers では、Dagger から Hilt にマイグレーションをする Codelab を用意しています。こちらを参考に導入を行なっていきます。
おおまかな対応内容は下記になります。
① ライブラリ導入
ルートプロジェクトの build.gradle ファイルに下記を追記します
plugins { id 'com.google.dagger.hilt.android' version '$hilt_version' apply false }
また使用する module の build.gradle ファイルに下記を追記します
plugins { id 'com.google.dagger.hilt.android' } dependencies { implementation "com.google.dagger:hilt-android:$hilt_version" kapt "com.google.dagger:hilt-compiler:$hilt_version" }
② Application クラスの対応
@HiltAndroidApp
のアノテーションを付与します。
@HiltAndroidApp class MainApplication : Application() {
③ Viewmodel クラスの対応
@HiltViewModel
アノテーションの付与します。
@HiltViewModel class MainViewModel @Inject constructor(〜) : ViewModel() {
④ Activity / Fragment クラスの対応
@AndroidEntryPoint
アノテーションの付与します。ViewModel は viewModels()
を使用して取得します。
また依存性の注入をしていた inject 処理は不要になったため削除します。
@AndroidEntryPoint class MainFragment : Fragment() { val viewModel: MainViewModel by viewModels() override fun onAttach(context: Context) { // AndroidSupportInjection.inject(this) <- 削除
⑤ Module クラスの修正
各 Module クラスにて、@InstallIn
でインジェクションの対象ごとに Component を指定します。
Hilt Component | インジェクションの対象 |
---|---|
SingletonComponent | Application |
ActivityComponent | Activity |
FragmentComponent | Fragment |
ViewComponent | View |
ServiceComponent | Service |
また、引数の Context
は @ApplicationContext
等のアノテーションで付与します。
下記はアプリ全体で使用するため、 @InstallIn
は SingletonComponent
を指定し、Context
は @ApplicationContext
を付与しています。
@Module @InstallIn(SingletonComponent::class) internal object RepositoryModule { @Singleton @Provides fun provideMainRepository(@ApplicationContext context: Context): MainRepository = MainRepositoryImpl(context)
⑥ 不要になった処理・クラスの削除
Fragment や ViewModel 等は Module クラスを作成して、Dagger の定義の管理をしていました。Hilt ではその定義が不要となったため削除します。
下記のような Module 定義を削除しました。
@Module abstract class ViewModelModule { @Binds @IntoMap @ViewModelKey(MainViewModel::class) abstract fun bindMainViewModel(mainViewModel: MainViewModel): ViewModel
遭遇した問題
問題 1 . Application クラスはルートモジュールでないと @HiltAndroidApp を付与することができない
対応中にビルドエラーが出ました
Application class annotated with @HiltAndroidApp has to be defined in an android application project
原因としては、モジュール分割対応で Application クラスを別モジュールに移動させてしまっていました。このタイミングで気づくとは・・
ルートモジュールに Application クラスを配置して解消しました。
問題 2 . Fragment の ViewModel 参照は onCreate 以降
画面表示時に強制終了しました。
IllegalStateException: You can consumeRestoredStateForKey only after super.onCreate of corresponding component
Fragment の onAttach で ViewModel にイベントを設定する処理を行なっていました。ViewModel は onCreate 前では ViewModel が作成されていないため、onAttach ではエラーになってしまいます。
処理を onCreate に移動し、エラーを解消しました。
override fun onAttach(context: Context) { viewModel.event = 〜
から次のように変更しました。
override fun onCreate(savedInstanceState: Bundle?) { viewModel.event = 〜
結果
Hilt 導入を終えてもっとも感じたことは、新規画面の追加実装が簡単になりました。特に忘れがちな Fragment や ViewModel の Module 定義が不要となったことが大きく、ストレスフリーに開発ができるようになりました。
新規画面の実装場合
対応前 | 対応後 |
---|---|
Fragment を作成する | Fragment を作成する |
FragmentModule に定義を追加する | ※ 省略 |
ViewModel を作成する | ViewModel を作成する |
ViewModelModule に定義を追加する | ※ 省略 |
UseCase / Repositoryを作成する | UseCase / Repositoryを作成する |
次は Hilt でテストを実装するぞ!