#53 AndroidでToDoアプリを作る - Hiltの導入
今回はDI(Dependency Injection)の概要とHiltの導入について説明します。
DI
公式ドキュメントによる解説はこちらにあります。
クラスAが役割を果たすために、クラスBのインスタンスを必要とする場合を考えます。リポジトリの回で作ったToDoRepositoryImpl
では、ToDoDAO
を必要としています。メソッドが呼ばれたときにToDoDAO
を使うので、次のようにプロパティとして保持しておきます。
class ToDoRepositoryImpl(): ToDoRepository {
// プロパティとしてもたせる
private val dao: ToDoDAO
}
メソッドが呼ばれる前に、このdao
プロパティに何か値をセットしておく必要があります。
1つ目の方法はToDoRepositoryImpl
自身でToDoDAO
を作ってセットする方法です。
class ToDoRepositoryImpl(): ToDoRepository {
private val dao: ToDoDAO = Room.databaseBuilder(
// Application Contextがとれるものとします
ToDoApplication.instance,
ToDoDatabase::class.java,
"todo.db"
).build().todoDAO()
}
この方法の場合、テスト時に
2つ目の方法はリポジトリの回でも採用したコンストラクタで受け取る方法です(1つ目の方法と比較しやすいよう、短縮した書き方にはしていません)。
class ToDoRepositoryImpl(dao: ToDoDAO): ToDoRepository {
private val dao: ToDoDAO = dao
}
この方法だと、テスト時にはモック実装を渡すことで簡単にテストが実施できるようになります。
3つ目の方法は次のようにプロパティを代入可能にした上で、インスタンス作成後に値をセットしてもらう方法です。
class ToDoRepositoryImpl(): ToDoRepository {
var dao: ToDoDAO? = null
}
2つ目と3つ目の方法は外部からインスタンスを受け取る形になっています。インスタンスを組み立てる際に依存するインスタンスを(何らかの方法で)持ってきて渡す(Injectする)ことから、DI(Dependency Injection)と呼びます。
自動インジェクション
依存性の注入は手動でも行うことができます。例えばToDoRepositoryImpl
を組み立てるには次のようなコードを書けばよいでしょう。
val db = Room.databaseBuilder(
context,
ToDoDatabase::class.java,
"todo.db"
).build()
val dao = db.todoDAO()
ToDoDatabase
を作るにはContext
が必要です。次のようなコードに修正する必要がありそうです。
val context = requireContext().applicationContext
val db = Room.databaseBuilder(
context,
ToDoDatabase::class.java,
"todo.db"
).build()
val dao = db.todoDAO()
依存関係はグラフになりますが、グラフが大きくなると組み立てる順序を考えたりするのが大変になります。そこで登場するのが自動インジェクションライブラリです。 AndroidではDaggerというライブラリがおすすめされていましたが、2020年にHiltというライブラリが登場し、状況が大きくかわりました。
Hilt
HiltとはDaggerの上で構築されているライブラリです。AndroidでDaggerを導入する標準的な方法を提供してくれます。「Daggerは便利だがとても難しい」という意見がありますが、Hiltはその難しさをかなり軽減してくれます。
公式ドキュメントはこちら(英語)にあります。さっそくToDoアプリにHiltを導入してみましょう。
ライブラリとプラグインを追加する
ToDoアプリにHiltを導入するには、まずトップレベルにあるbuild.gradleにhilt-android-gradle-pluginプラグインを追加します。
buildscript {
...
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
// この行を追加
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.31-alpha'
}
}
次に、app/build.gradleにdagger.hilt.android.pluginを追加します。
apply plugin: 'kotlin-kapt'
// この行を追加
apply plugin: 'dagger.hilt.android.plugin'
そしてdependenciesに次の2行を追加します。
dependencies {
...
implementation "com.google.dagger:hilt-android:2.31-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.31-alpha"
}
HiltはJava 8の機能を使用するので、その設定もapp/build.gradleに記述します。本シリーズを順に読み進めている方は前回設定済みだと思うので説明は省略します。
アプリケーションクラスを作る
Hiltを使う場合、@HiltAndroidApp
アノテーションのついたアプリケーションクラスを用意し使う必要があります。中身は空っぽでいいので次のようなクラスを作ります。
package com.mokelab.mytodo
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class ToDoApplication: Application() {
}
アプリケーションクラスはAndroidManifest.xmlのapplication
要素のandroid:name
属性で指定しないと使用されないので、こちらも修正します。
<manifest>
<application
...
android:theme="@style/AppTheme"
android:name=".ToDoApplication">
...
</application>
</manifest>
@AndroidEntryPointをつける
次に、アクティビティとフラグメントに@AndroidEntryPoint
アノテーションをつけていきます。これにより、Hiltが依存関係を調べて注入してくれるようになります。今回は例としてMainActivity
とMainFragment
につけてみます。他のフラグメントにも同様にアノテーションを付けておきます。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
@AndroidEntryPoint
class MainFragment: Fragment(R.layout.main_fragment) {
private val vm: MainViewModel by viewModels()
}
インスタンスの作り方をHiltに伝える
次は依存性注入をされる側の準備です。インスタンスを作る際、依存するインスタンスをHiltにセットしてもらいたい場合、次のようにコンストラクタに@Inject
アノテーションをつけます。
class ToDoRepositoryImpl @Inject constructor(
private val dao: ToDoDAO
): ToDoRepository {
...
}
ToDoRepositoryImpl
のコンストラクタはToDoDAO
を必要としているので、HiltはToDoRepositoryImpl
を作る際、ToDoDAO
をどこからか持ってきてセットしてくれます。
Hiltモジュール
さきほど、「ToDoDAO
をどこからか持ってきて」と書きましたが、ToDoDAO
はRoom経由で作るインスタンスなのでHiltは作り方を知りません。そのような場合にはHiltモジュールを作ります。
Hiltモジュールを作るには、次のように@Module
と@InstallIn
アノテーションをつけたobjectを作ります。
(2021年2月4日追記)バージョン2.28-alphaの時点では、アプリ全体で使うモジュールはApplicationComponent
という名称でしたが、
バージン2.31-alphaで名称がSingletonComponent
に変更されました。
@Module
@InstallIn(SingletonComponent::class)
object ToDoModule {
@Singleton
@Provides
fun provideToDoDAO(db: ToDoDatabase): ToDoDAO {
return db.todoDAO()
}
}
メソッドとして、ToDoDAO
を返し、@Singleton
と@Provides
アノテーションを付けたメソッドを作ります。Daggerの慣例としてprovideで始まるメソッド名にしておきます。引数にはToDoDAO
を作るのに必要なものがあれば記述します。
ToDoDatabase
の作り方もHiltは知らないので同様に教えてあげます。Context
を引数として受け取りたい場合、@ApplicationContext
アノテーションをつけるとHiltはアプリケーションコンテキストを渡してくれます。
@Module
@InstallIn(SingletonComponent::class)
object ToDoModule {
@Singleton
@Provides
fun provideToDoDatabase(
@ApplicationContext context: Context
): ToDoDatabase {
return Room.databaseBuilder(
context,
ToDoDatabase::class.java,
"todo.db"
).build()
}
@Singleton
@Provides
fun provideToDoDAO(db: ToDoDatabase): ToDoDAO {
return db.todoDAO()
}
}
また、ToDoRepository
インターフェースなインスタンスを要求されたとき、何を作って返せばいいかもHiltに教えます。こちらは抽象クラスとして作ります。
@Module
@InstallIn(SingletonComponent::class)
abstract class ToDoRepositoryModule {
@Singleton
@Binds
abstract fun bindToDoRepository(
impl: ToDoRepositoryImpl
): ToDoRepository
}
bindToDoRepository
メソッドの戻り値の型がToDoRepository
で、引数の型がToDoRepositoryImpl
となっているため、Hiltは「ToDoRepository
なインスタンスを要求されたら、ToDoRepositoryImpl
を渡せばよい」と判断してくれるようになります。やや黒魔術感がありますが、そういうものとして捉えてください。
ビルドしてみる
Hiltによるコード生成はビルド時に行われます。ここで一旦ビルドしてみましょう。Android Studioのメニューから「Build」→「Make Module」を選びます。
すると、次のようなエラーがでます(長いので適度に改行をいれています)。
> Task :app:kaptDebugKotlin
/Users/fkm/androidapp/MyToDo/app/build/tmp/kapt3/stubs/debug/com/mokelab/mytodo/model/todo/ToDoDAO.java:11:
エラー: To use Coroutine features, you must add `ktx` artifact from Room as a dependency. androidx.room:room-ktx:<version>
...
Roomに関するエラーが出ました。Roomの回でビルドすれば気づけたエラーですが、room-ktxの追加が必要でした。すいません。。。
気を取り直してapp/build.gradleにライブラリを追加します。
dependencies {
...
// この行を追加
implementation 'androidx.room:room-ktx:2.2.5'
implementation 'androidx.room:room-runtime:2.2.5'
kapt 'androidx.room:room-compiler:2.2.5'
}
ライブラリを追加したら再度ビルドしてみます。次のようにBUILD SUCCESSFULと出れば成功です。
BUILD SUCCESSFUL in 804ms
まとめ
DIがどのようなものかと、Hiltの導入方法を紹介しました。手順はやや多めで多少慣れも必要ですが、アプリの規模が大きくなるにつれ恩恵も大きくなるので、怖がらずに導入してみましょう。
更新履歴
- 2020-12-18 初版
- 2021-2-4 ライブラリのバージョンアップとSingletonComponentへの修正