14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Koinを使っているマルチモジュールプロジェクトをHiltに移行してみた

Last updated at Posted at 2020-08-08

はじめに

会社のアプリ個人的なアプリもマルチモジュールプロジェクトでDIフレームワークにはKoinを使っています。Googleさんお勧めのDaggerは難解でメンバーへの展開が難しかっため、直感的に理解しやすいKoinの方を採用していました。

しかし、Android Studio 4.1(初稿執筆の2020年8月時点ではBeta)ではDaggerに対応したソースコードのナビゲーション機能が追加されたらしく、Koinではこのようなことはできないので、Koinを採用したことを軽く後悔したできことがありました。

また先日のAndroid 11 Meetupsで、Jetpackの一部としてDaggerベースのHiltがリリースされてDaggerよりも簡単に使えるという内容のセッションがありました。

それらを受けて、Koinを使っている個人的なアプリについて、Hiltに移行してみました。

2021年6月13日追記

新しく別の個人開発Androidアプリの開発を開始しましたが、Dagger Hiltのバージョンが2.36になり、2020年8月に書いたこの記事の内容と差異が発生していることが分かったので、現在のバージョンで変更がある部分は補足説明を加えました。

KoinよりHiltを使うメリット

Koin採用済みのある程度規模のある実プロダクトで、わざわざHiltに移行しなくても良いと思います。私の今回の移行作業は学習のためというモチベーションが大きいです。しかし新規プロダクトやこれからDIを採用するプロダクトにおいてはKoinではなくHiltを使うと以下のメリットがあると思います。

  • 依存性注入のModuleを探しやすくなる(HiltはAndroid Studio 4.2からの対応)
  • 依存性を解決できないことで発生する実行時エラーは起こらない
  • KotlinとJavaで依存するインスタンスの取得方法が同じになる

Hiltに移行するアプリの解説

今回、KoinからHiltに移行するアプリはシンプルな音声の録音再生アプリです。
画面回転や画面分割に対応しています。

クラスの紹介

今回解説する範囲における、クラスの依存関係はこのようになっています。

スクリーンショット 2020-08-08 18.12.39.png

MainFragment

Fragmentです。画面回転や画面分割で再生成されます。

RecordViewHelper

録音のために録音デバイス制御などを行います。ViewModelを継承しているので画面回転や画面分割が起きても同じインスタンスが保持されます。

PlayViewHelper

再生のために音声デバイス制御などを行います。RecordViewHelperと同様にViewModelを継承していて、画面回転や画面分割が起きても同じインスタンスが保持されます。

SoundRepository

録音音声の保持と取り出しを担当します。シングルインスタンスです。音声がメモリー、ローカルファイル、クラウド等どこにあるか呼び出し元は関与しません。

SoundMemoryLocalDataStore

録音音声をメモリーに保持する担当です。シングルインスタンスです。

SoundFileLocalDataStore

録音音声をAACにエンコードしてローカルファイルファイルに保持します。シングルインスタンスです。プロセスキルからの復帰ではこちらが使われます。

モジュールの紹介

レイヤードアーキテクチャへの強制力を持たせるために、レイヤー別マルチモジュールプロジェクトになっています。下図のように役割に応じてクラスをモジュールに配置しています。

QiitaのHiltの記事.png

移行の手順

ライブラリ追加

ほぼ公式情報の手順です。

プロジェクトルートの build.gradle ファイルに hilt-android-gradle-plugin プラグインを追加します。

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

私のプロジェクトでは Minimum supported Gradle version is 5.6.4. Current version is 5.4.1. エラーが表示されたので、Gradleバージョンを上げました。

gradle/wrapper/gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

マルチモジュールプロジェクトなので、各プロジェクトへのライブラリの追加のために共通のgradleファイルの方を修正しました。

gradle/common.gradle
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
// 略
dependencies {
    // 略
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

gradle/common.gradle ファイルは以前からこのように取り込んでいます。

localDataStore/build.gradle
apply from: rootProject.file('gradle/common.gradle')

ViewModelも注入したいため、公式のこちらの文献に沿って、さらにライブラリを追加します。

gradle/view-common.gradle
apply from: rootProject.file('gradle/common.gradle')
// 略
dependencies {
    // 略
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
    kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
}

ViewModelを含むmainモジュールではこのように取り込んでいます。

main/build.gradle
apply from: rootProject.file('gradle/view-common.gradle')

2021年6月13日追記

Daggerバージョン2.31.1から androidx.hilt:hilt-lifecycle-viewmodel ライブラリは不要になりました。
Daggerバージョン2.34のリリースノートのMigration stepsも参考になります。

モジュール依存関係変更

Hiltはアプリケーションモジュールから直接参照しているモジュールにあるクラスしか注入できないので、このようにモジュールの依存関係を変更しました。

スクリーンショット 2020-08-08 20.17.35.png

app/build.gradle
dependencies {
    // 略
    implementation project(':repository')
    implementation project(':localDataStore')
}

Koinでは直接参照していないモジュールにあるクラスを注入できるのですが、その件は補足で説明します。

2021年6月13日追記

Daggerバージョン2.31からアプリケーションモジュールから直接参照されてないモジュールにあるクラスも注入できるようになりました。

アプリケーションモジュールのbuild.gradleにこちらを追加します。

app/build.gradle
hilt {
    enableExperimentalClasspathAggregation = true
}

参考にした情報

Daggerバージョン2.37について

Daggerバージョン2.37からは enableExperimentalClasspathAggregation フラグの代わりに enableAggregatingTask フラグを使うようです。しかし単体テストでビルドエラーが発生してしまい、執筆時点ではリリースから3日しか経ってなかったので、今回は見送りました。

Applicationクラスにアノテーション追加

すでにKoinのためにApplicationを継承したクラスを定義していました。
そのクラスに @HiltAndroidApp アノテーションを追加します。

QuickEchoApplication.kt
@HiltAndroidApp
class QuickEchoApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // 略
    }
}

モジュールを作成する

SoundMemoryLocalDataStore、SoundFileLocalDataStore、SoundRepositoryを注入できるように、Hiltのモジュールを作成します。
MockKでモックを作って単体テストできるように、インターフェースと実装を分けています。

HiltLocalDataStoreModule.kt
// appモジュールまたはlocalDataStoreモジュールに置く
@Module
@InstallIn(ApplicationComponent::class)
abstract class HiltLocalDataStoreModule {
    @Binds
    @Singleton
    abstract fun bindSoundMemoryLocalDataStore(
            soundMemoryLocalDataStore: SoundMemoryLocalDataStoreImpl
    ): SoundMemoryLocalDataStore

    @Binds
    @Singleton
    abstract fun bindSoundFileLocalDataStore(
            soundFileLocalDataStore: SoundFileLocalDataStoreImpl
    ): SoundFileLocalDataStore
}
HiltRepositoryModuke.kt
// appモジュールまたはrepositoryモジュールに置く
@Module
@InstallIn(ApplicationComponent::class)
abstract class HiltRepositoryModule {
    @Binds
    @Singleton
    abstract fun bindSoundRepository(
            soundRepository: SoundRepositoryImpl
    ): SoundRepository
}

注入されるクラスのコンストラクタには @Inject アノテーションを付けます。
アプリケーションコンテキストは @ApplicationContext アノテーションで注入できます。

SoundMemoryLocalDataStore.kt
class SoundMemoryLocalDataStoreImpl
@Inject constructor(@ApplicationContext private val context: Context) : SoundMemoryLocalDataStore {
    // 略
}
SoundFileLocalDataStore.kt
class SoundFileLocalDataStoreImpl
@Inject constructor(@ApplicationContext private val context: Context) : SoundFileLocalDataStore {
    // 略
}
SoundRepository.kt
class SoundRepositoryImpl
@Inject constructor(private val soundMemoryLocalDataStore: SoundMemoryLocalDataStore,
private val soundFileLocalDataStore: SoundFileLocalDataStore) : SoundRepository {
}

2021年6月13日追記

Daggerバージョン2.28.2からApplicationComponentクラスはSingletonComponentクラスになりました。

ViewModelを注入する

ViewModelのコンストラクタに @ViewModelInject アノテーションを付けます。

RecordViewHelper.kt
class RecordViewHelper @ViewModelInject constructor(
        private val repository: SoundRepository) : ViewModel() {

注入されるActivityとFragmentに @AndroidEntryPoint アノテーションを付けます。Activityでは注入を行ってなくても、Activityが持つFragmentに対して注入を行っている場合はActivityにもアノテーションを付けないと実行時エラーになります。

MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    // 略
}
MainFragment.kt
@AndroidEntryPoint
class MainFragment : Fragment() {
    // 略
}

ここまで定義すると、このようにするだけでViewModelが依存するインスタンスとともにViewModelが生成されます。

MainFragment.kt
@AndroidEntryPoint
class MainFragment : Fragment() {

    private val recordViewHelper: RecordViewHelper by viewModels()

    private val playViewHelper: PlayViewHelper by viewModels()

    // 略
}

2021年6月13日追記

Daggerバージョン2.31.1からViewModelクラスには @HiltViewModel アノテーションを付けて、そのコンストラクタには @Inject アノテーションを付けるようになりました。
Daggerバージョン2.34のリリースノートのMigration stepsも参考になります。

RecordViewHelper.kt
@HiltViewModel
class RecordViewHelper @Inject constructor(
        private val repository: SoundRepository) : ViewModel() {

補足

Koinでアプリケーションモジュールから直接参照していないモジュールにあるクラスを注入できるようにする

一番下のレイヤーのモジュールです。

LocalDataStoreModule.kt
val localDataStoreModule = module {
    single { SoundMemoryLocalDataStoreImpl(androidContext()) as SoundMemoryLocalDataStore }
    single { SoundFileLocalDataStoreImpl(androidContext()) as SoundFileLocalDataStore }
}

その上のレイヤーのモジュールでは、さらに上のレイヤーのモジュールから下のレイヤーのモジュールを参照できるようにします。

RepositoryModule.kt
val localDataStoreModuleInRepositoryModule = localDataStoreModule
val repositoryModule = module {
    factory { SoundRepositoryImpl(get(), get()) as SoundRepository }
}

その上のレイヤーでも同様にします。

MainModule.kt
val localDataStoreModuleInMainModule = localDataStoreModuleInRepositoryModule
val repositoryModuleInMainModule = repositoryModule
val mainModule = module {
    viewModel { RecordViewHelper(get()) }
    viewModel { PlayViewHelper(get()) }
}

アプリケーションモジュールにある、startKoinブロックではすべてのモジュールを指定します。

KoinSetting.kt
object KoinSetting {
    fun start(application: Application) {
        startKoin {
            androidContext(application.applicationContext)
            modules(listOf(localDataStoreModuleInMainModule,
                    repositoryModuleInMainModule,
                    mainModule))
        }
    }
}

全体ソースコード

プルリクはこちらです。
この記事では @Bindsを使用したインターフェースからのインスタンス注入しか説明してないですが、 @Provides を使用したインスタンス注入もこちらにあります。

14
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?