5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DI ライブラリ Metro の導入メモ

5
Last updated at Posted at 2026-01-02

概要

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
}

5
3
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
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?