[Android] ViewModelのRepositoryをHiltでDI


前置き

RepositoryをHiltでDIするサンプルコードが少なくてちょっと苦戦したので、メモも兼ねて記事にしようと思いました。今回はKotlinで説明しますが、Javaでも可能です。

まず最初に、RepositoryをDIする必要性について説明させてください。
(ソースコードだけ見たい人はマウスのコロコロを2回転半くらいしてください。)

RepositoryをDIする必要性

結論から言うと、やるに越したことはないですが目的がなければやらなくても良いかなと思います。

Repositoryとは

まず、Repositoryパターンについてですが、以下の図のような構成となっています。

RepositoryはローカルDB/WebAPIへのアクセスをViewModelから隠蔽する役割を持ちます。
これにより、ViewModelはRepositoryに対して「〇〇のデータをください」と要求をするだけで欲しいデータがもらえます。
ローカルDB/WebAPIどちらから取得するかはRepositoryが判断します。
また、DBの変更やWebAPIの変更の影響はRepositoryが吸収できるので、ViewModelは受けづらくなります。

Clean Architectureについて

次にClean Architectureですが、以下の図のような概念で構成されます

※引用元

詳しいことはググって頂きたいのですが、要するに変更されにくいビジネスロジックに向かって変更されやすいレイヤー(DBやUI)が依存するような構成にしようね、ということです。

AndroidのViewModelはビジネスロジックを含む(あまり良くない気はしますが)ようなので、ViewModel→Repositoryの依存関係をやめるためにDIが必要となります。
これにより変更に強くなったり、一番重要なビジネスロジックが独立して動くようになるのでテストがしやすくなったり、様々なメリットがあります。
このようなメリットが要らない(動けばよい)のであればDIする必要はないです。

参考:ViewModelはビジネスロジックを含む

 ViewModel オブジェクトは、特定の UI コンポーネント(フラグメントやアクティビティなど)のデータを提供します。また、モデルとやり取りするデータ処理のビジネス ロジックを含んでいます。
※引用元

実装

Gradle

    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01'
    kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'

ViewModel

class SampleViewModel @ViewModelInject constructor(
    private val mRepository: ISampleRepository
) : ViewModel() {
    val dataList = mRepository.getDataList()
}

ポイント

  • コンストラクタに@ViewModelInjectのアノテーションをつける
  • アノテーションをつけるときはconstructorが必要(省略記法は不可)
  • DIしたいオブジェクト(今回はISampleRepository)をパラメータで渡す
  • ISampleRepositoryはインタフェース(後述)

@VieModelInjectをつけると、ViewModelで必要なオブジェクト(ISampleRepository)をDI出来るようにになります。

この時点でViewModelはRepositoryに依存していない(インタフェースに依存している)状態になっています。

Repository(インタフェース)

interface ISampleRepository {
    fun getDataList(): LiveData<List<SampleData>>
}

Repository(実装クラス)

class SampleRepository @Inject constructor() : ISampleRepository  {
    @Inject
    lateinit var mDao: SampleDataDao

    override fun getDataList(): LiveData<List<SampleData>> {
        return // 省略
    }
}

ポイント

  • コンストラクタに@Injectをつける(つけ忘れ注意)
  • アノテーションをつけるときはconstructorが必要(省略記法は不可)
  • DIしたいオブジェクトに@Injectをつける

補足として、今回はRoomを使う想定で書いています。
mDaoはRoomDatabaseへのDAOだと思って読んでください。

先述した通りViewModelはRepositoryに依存していませんが、当然、Repositoryのインスタンスをどうやって作るか(インタフェースに対する実装をどうするのか)を定義しないといけませんので、後述します。

Module

@Module
@InstallIn(ApplicationComponent::class)
abstract class RepositoryModule {
    @Binds
    @Singleton
    abstract fun bindStampRepository(impl: StampsRepository): IStampRepository
}

ポイント

  • abstract class
  • @Moduleをクラスにつける
  • @InstallInをクラスにつける
  • @InstallInの中は公式参照。大体の場合、ApplicationComponent(シングルトン)でよいと思います
  • abstaract fun
  • メソッドに@Bindsをつける
  • メソッドに@Singletonをつける(InstallInでなにを選んだかに依存)
  • メソッド名は任意(クラス内で重複は不可)
  • メソッドの引数が実装クラス、戻り値がインタフェース

ここでRepositoryのインスタンス生成方法を定義します。
シングルトンでよい場合は、ApplicationComponentです。
Activity単位やFragment単位が良ければ、適切なComponentを指定してください。

[Appendix] Dao(Room)のModule

RepositoryのDIではないのですが、HiltでDIする上でよく使う書き方があるので、紹介させてください。

@Module
@InstallIn(ApplicationComponent::class)
class DatabaseModule {
    @Provides
    @Singleton
    fun getDatabase(@ApplicationContext context: Context): SampleDatabase {
        return Room.databaseBuilder(
            context.applicationContext,
            SampleDatabase::class.java,
            "sampledata"
        ).build()
    }

    @Provides
    @Singleton
    fun getDao(db: SampleDatabase): SampleDataDao {
        return db.sampleDataDao
    }
}

ポイント

  • コンストラクタに@Injectをつけれない(Room等の外部モジュール)場合、Providesが使える
  • Providesを使うと、インスタンス生成のロジックを記述することができる
  • 引数に@ApplicationContextをつけると、ApplicationContextを取得できる
  • Moduleとして登録したものは、メソッドの引数(今回で言うとgetDatabaseメソッドで定義したSampleDatabaseを「getDao(db: SampleDatabase」))として渡せる

ちなみに、InstallInを@ActivityComponent,メソッドのスコープを@ActivityScopedにすると、@ActivityContextでActivityContextが取得できます

Fragment

@AndroidEntryPoint
class SampleFragment : Fragment() {
    private val viewModel: SampleViewModel by viewModels()
    // 以下省略

ポイント

  • クラスに@AndroidEntryPointをつける
  • by viewModels()すると勝手にViewModelに必要なものをDIしてくれる(すごすぎ...)

@AndroidEntryPointつけ忘れると、結構ハマります。

Activity

@AndroidEntryPoint
class SampleActivity : AppCompatActivity() {
    // 以下省略

ポイント

Application

@HiltAndroidApp
class MainApplication: Application() {}
  • クラスに@HiltAndroidAppをつける
  • とにかくつけないとHiltが動かない

(書き忘れたことがなければ)これで以上です!!