概要
KMP でも使うことができる DI ライブラリの Metro を Android ネイティブ に導入してみた時の簡易メモ。
(バージョン 0.9.2)
Metro を使う理由
- kotlin ネイティブ
- KMP プロジェクトでも使える
- Hilt に比べてビルドプロセスに余分なステップが無いため、ビルド時間が数倍早い
- Cash Android Moves to Metro 記事の中でも Dagger と比較してベンチマークによるビルド速度の向上例が記載されている
導入
プラグインを追加する。
plugins {
id("dev.zacsweers.metro" version "x.x.x")
}
依存グラフを定義する
依存グラフはオブジェクトグラフへのエントリポイントとなる。
@DependencyGraph を付与した interface として定義する。
基本的には一つのグラフに複数の機能を集約する設計方針であり、バインドするスコープごとにグラフが増える。
@DependencyGraph(AppGraph::class)
interface AppGraph {
val repository: Repository
}
@Inject
class Repository {}
@Inject
class HogeViewModel(val repository: Repository): ViewModel {}
一般的な DI では、実装をインタフェースにバインドする。
これを実現するため、Metro では以下の2パターンがある。
集約なし
グラフ内で実装をインタフェースに明示的にバインドする。
interface Repository {}
@Inject
class RepositoryImpl(): Repository {}
@DependencyGraph
interface AppGraph {
@Binds val RepositoryImpl.bind: Repository
}
@Inject
class ExampleViewModel(private val repository: Repository): ViewModel() {}
集約あり
@ContributesBinding で指定されたスコープを持つグラフにインタフェースの実装が提供される。
interface Repository {}
interface Base {}
@Inject
@ContributesBinding(AppScope::class)
class RepositoryImpl(): Repository {}
// 複数のスーパータイプを持つ場合は、バインドする型を明示的に指定する
@Inject
@ContributesBinding(scope = AppScope::class, binding = binding<Base>())
class RepositoryMultiImpl(): Repository, Base {}
@Inject
class ExampleViewModel(private val repository: Repository): ViewModel() {}
@Includes を用いることで別のグラフや通常のクラスをグラフに含めることができる。
サードパーティクラスの提供
ライブラリ等に含まれるクラスの場合、明示的にアノテーションを付与することができない。
それらを DI させたい場合、@Providers を用いる。
@Providers を付与した関数は明示的な戻り値の方を定義する必要がある。
また、同じ戻り値を持つプロバイダー関数が複数ある場合、@Named(String) を付与することで型の曖昧性を解消することができる。
@ContributesTo(AppScope::class)
interface NetworkProviders {
@Provides
fun provideCache(application: Application): Cache =
Cache(application.cacheDir.resolve("http_cache"), 50L * 1024 * 1024)
@Provides
fun provideOkHttpClient(cache: Cache): OkHttpClient =
OkHttpClient.Builder()
.cache(cache)
.build()
}
@Named("named") val namedInt: Int
@Providers
fun provideInt(): Int = 0
@Providers
@Named("named")
fun provideNamedInt(): Int = 1
スコープ
@SingleIn(AppScope::class) を付与することで、指定したスコープのグラフごとにインスタンスが一つだけ存在することを保証できる。
@Inject
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DatabaseImpl: Database
逆に DI の度に新しいインスタンスを生成させたい場合、集約ありであれば @Inject @ContributesBinding を付与するのみで良い。
ViewModel の DI
1, 以下の依存関係を追加する
implpementation("dev.zacsweers.metro:metrox-android:x.y.z")
implementation("dev.zacsweers.metro:metrox-viewmodel:x.y.z")
implementation("dev.zacsweers.metro:metrox-viewmodel-compose:x.y.z")
2, DI Graph で ViewModelGraph を継承させる
@DependencyGraph(AppScope::class)
interface AppGraph: MetroAppComponentProviders, ViewModelGraph
警告
MetroAppComponentProviders は Activity/Fragment/Service 等への DI を担う。
minSDK=28 以下では機能しない。
3, ViewModelFactory を実装する
@Inject
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class MyViewModelFactory(
override val viewModelProviders: Map<KClass<out ViewModel>, Provider<ViewModel>>,
override val assistedFactoryProviders: Map<KClass<out ViewModel>, Provider<ViewModelAssistedFactory>>,
override val manualAssistedFactoryProviders: Map<KClass<out ManualViewModelAssistedFactory>, Provider<ManualViewModelAssistedFactory>>,
) : MetroViewModelFactory()
4, Application クラスでグラフを生成する
class App : Application(), MetroApplication {
override val appComponentProviders: MetroAppComponentProviders by lazy { createGraph<AppGraph>() }
}
5, Activity に ViewModelFactory を依存注入し、Composition Local にて meteoViewModel() から factory へアクセスできる様にする
@ContributesIntoMap(AppScope::class, binding<Activity>())
@ActivityKey(MainActivity::class)
@Inject
class MainActivity(private val metroVmf: MetroViewModelFactory) : ComponentActivity() {
...
App(metroVmf = metroVmf)
}
@Composable
private fun App(
metroVmf: MetroViewModelFactory,
modifier: Modifier = Modifier,
) {
CompositionLocalProvider(LocalMetroViewModelFactory provides metroVmf) {
...
}
}
6, ViewModel を DI できる様にする
画面遷移時、引数を持たない場合は以下。
@Inject
@ViewModelKey(ExampleViewModel::class)
@ContributesIntoMap(AppScope::class)
class ExampleViewModel : ViewModel() {
...
}
@Composable
fun Example(
modifier: Modifier = Modifier,
viewModel: ExampleViewModel = metroViewModel(),
) {
...
}
画面遷移時、引数を持たせる場合は以下。
ManualViewModelAssistedFactory を使う方法と ViewModelAssistedFactory を使う方法がある。
ManualViewModelAssistedFactory の場合
@Inject
class ExampleViewModel(@Assisted val id: String): ViewModel() {
@AssistedFactory
@ManualViewModelAssistedFactoryKey(Factory::class)
@ContributesIntoMap(AppScope::class)
fun interface Factory : ManualViewModelAssistedFactory {
fun create(@Assisted title: String): ExampleViewModel
}
}
@Composable
fun Example(
viewModel: ExampleViewModel = assistedMeteroViewModel<ExampleViewModel, ExampleViewModel.Factory> { create("id") }
) { }
ViewModelAssistedFactory の場合
@Inject
class ExampleViewModel(@Assisted val id: String): ViewModel() {
@AssistedFactory
@ViewModelAssistedFactoryKey(ExampleViewModel::class)
@ContributesIntoMap(AppScope::class)
fun interface Factory: ViewModelAssistedFactory {
override fun create(extras: CreationExtras): ExampleViewModel {
val id: String = extras[IdKey] ?: error("id の取得に失敗")
return create(title)
}
fun create(@Assisted id: String): ExampleViewModel
}
companion object {
val IdKey = object : CreationExtras.Key<String> {}
}
}
@Composable
fun Example(
viewModel: ExampleViewModel = assistedMetroViewModel(
MutableCreationExtras().apply { set(ExampleViewModel.IdKey, "id") }
)
) { }
基本的には ManualViewModelAssistedFactory の方で良い。
metrox-android を使って Activity に ViewModelFactory を DI する方法以外に graph から直接渡す方法がある。この場合、minSDK >= 28 の制約はなくなる。
class App : Application() {
val appGraph: AppGraph by lazy { createGraph<AppGraph>() }
}
val Context.viewModelFactory: MetroViewModelFactory
get() = (applicationContext as App).appGraph.viewModelFacto
class MainActivity : ComponentActivity() {
private val metroVmf: MetroViewModelFactory by lazy { viewModelFactory }
override val defaultViewModelProviderFactory: ViewModelProvider.Factory
get() = metroVmf
}