Mokelab Blog

#43 AndroidでHiltを使ってみる

Android用のDI(Dependency Injection)ライブラリであるHiltの使い方を説明してみます。

公式ドキュメントはこちらです。

DIとは

ある程度の規模のプロジェクトになると、 「あるクラスの実装を行う際、他クラスのオブジェクトが必要」という場面に遭遇することが多くなります。 この関係を依存関係と呼びます。

もうすこし具体的な例で見てみましょう。ToDoデータを管理するTodoRepositoryImplは、 サーバーとの通信にOkHttpClientを必要とするものとします。TodoRepositoryImpl内で直接OkHttpClientオブジェクトを作ってもいいのですが、ここでは次のようにコンストラクタの引数として受け取ることにします。

class TodoRepositoryImpl(private val client: OkHttpClient) { 
  ... 
}

そして、TodoRepositoryImplを使う場面で、次のようにOkHttpClientを渡してオブジェクトを作ります。これが依存性注入(DI)と呼ばれる手法です。

val todoRepo = TodoRepositoryImpl(OkHttpClient())

依存するオブジェクトを差し込むには、次の2つの方法があります。

本記事のゴール

本記事では、Hiltを使ってViewModelに「ToDoRepositoryインターフェースを実装するオブジェクト」を差し込むことをゴールにします。

Hiltを追加する

※本節は公式ドキュメントほぼそのまんまの内容となります。

まず、プロジェクトのルートにあるbuild.gradleに、hilt-android-gradle-pluginを追加します。

buildscript {
  ...
  dependencies {
      ...
      classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
  }
}

次に、app/build.gradleにkaptとhiltプラグインを追加します。

apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

app/build.gradleのdependenciesにライブラリを追加します。

dependencies {
  // by viewModels()を使いたい
  implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha06'

  implementation "com.google.dagger:hilt-android:2.28-alpha"
  kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"

  // ViewModelでHiltを使うために追加
  implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01'
  kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'  
}

Hiltや、by viewModels()はJava 8の機能を使っているので、そのための設定も追加します。

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}  

アプリケーションクラスを追加する

@HiltAndroidAppのついたアプリケーションクラスを追加します。中身は空っぽでOKです。

@HiltAndroidApp
class MyApplication : Application() { ... }

アプリケーションクラスはAndroidManifest.xmlで使うよう指定しましょう。

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.mokelab.demo.hilt">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:name=".MyApplication">
        ...
    </application>
</manifest>

@AndroidEntryPointをつける

アクティビティとフラグメントに対し、@AndroidEntryPointのアノテーションをつけます。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() { ... }

フラグメントにだけアノテーションを付け、親となるアクティビティにアノテーションを付けなかった場合、実行時に次のような例外が発生します。

java.lang.RuntimeException: Unable to start activity ComponentInfo{<コンポーネント名>}:
   java.lang.IllegalStateException: Hilt Fragments must be attached to an @AndroidEntryPoint Activity.
   Found: class com.mokelab.demo.hilt.MainActivity

ViewModelをフラグメントに持たせる

ここは、フラグメントに@AndroidEntryPointがついていること以外は通常のViewModelと同じです。

@AndroidEntryPoint
class MainFragment : Fragment(R.layout.main_fragment) {
  ...  
  private val vm: MainViewModel by viewModels()
  ...
}

ViewModelのコンストラクタをHilt対応版にする

@ViewModelInject constructorにします。このアノテーションはandroidx.hilt:hilt-lifecycle-viewmodelで提供されています。

class MainViewModel @ViewModelInject constructor(
  private val repo: ToDoRepository
) : ViewModel() {

Hiltのモジュールを追加する

この時点でビルドしようとすると、次のようなビルドエラーが発生します。

エラー: [Dagger/MissingBinding] com.mokelab.demo.hilt.model.ToDoRepository cannot be provided 
  without an @Provides-annotated method.
  public abstract static class ApplicationC implements HiltApplication_GeneratedInjector,

MainViewModelToDoRepositoryを必要としているけど、インターフェースなのでどうやってインスタンス作ればいいかわからないゾ」 というエラーになります。

ここで登場するのがHiltのモジュールです。先にコードを見てみましょう。

@Module
@InstallIn(ApplicationComponent::class)
object MainModule {

  @Provides
  @Singleton
  fun provideOkHttpClient() = OkHttpClient()
  
  @Provides
  @Singleton
  fun provideToDoRepository(client: OkHttpClient): ToDoRepository {
    return ToDoRepositoryImpl(client)
  }
}

@Module@InstallIn()を付けます。各メソッドでは、どの型のオブジェクトが要求されたら、どれを返すかを実装します。 各メソッドには@Providesを付け、 オブジェクトを1つだけ作って欲しい場合は@Singletonも付けます。もしオブジェクトを作る際、他のオブジェクトが必要となる場合は メソッドの引数にします。

以上でHiltを使ったDIは完了です。ビルドし、動作確認をしてみましょう。

まとめ

Hiltを使った実践的なDIのやり方を説明しました。本記事執筆時ではアルファ版ですが、素のDaggerよりもだいぶ使いやすくなっているので、 導入してみましょう。

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