Mokelab Blog

#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()
}

この方法の場合、テスト時にdaoプロパティの中身を差し替えるのが困難になるという問題があります。

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が依存関係を調べて注入してくれるようになります。今回は例としてMainActivityMainFragmentにつけてみます。他のフラグメントにも同様にアノテーションを付けておきます。

@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の導入方法を紹介しました。手順はやや多めで多少慣れも必要ですが、アプリの規模が大きくなるにつれ恩恵も大きくなるので、怖がらずに導入してみましょう。

更新履歴

本サイトではサービス向上のため、Google Analyticsを導入しています。分析にはCookieを利用しています。