Dagger2.31以降のViewModelのDIについてメモ

Daggerを使ってViewModelをDIするには@Multibindingsを使う方法を始めとしていくつか考えられますが、ここでは@MultibindingsSubComponentなどは使わずに、ファクトリークラスを自分で用意する方法を、Dagger2.31で導入されたAssitedInjectも絡めてメモしておきます。

なお、この記事ではHilt, Dagger-Androidは扱いません。

AssistedInjectを使わない実装

以下のようなカスタムファクトリークラスを自分で用意します。

class BookViewModel(
    private val bookId: String,
    private val bookRepository: BookRepository
) : ViewModel() {
    ... ...

    class Factory @Inject constructor(
        private val bookRepository: BookRepository
    ) {
        fun create(
            val bookId: String
        ): BookViewModel { 
            return SampleViewModel(bookId, bookRepository)
        }
    }
}

UI側では以下のようにしてBookViewModelを生成できます。

@Inject
lateinit var bookViewModelFactory: BookViewModel.Factory
private val bookViewModel by viewModels<BookViewModel>(
    factoryProducer = {
        viewModelFactory {
            bookViewModelFactory.create("12345")
        }
    }
)

ここでは簡単のためbookIdとして12345という固定値を渡していますが、実際にはSafe Argsなどから取得した値を渡すことなどが多いかと思います。

なお、viewModelFactoryは次のようなメソッドです。

fun <T : ViewModel> viewModelFactory(
    factory: () -> T
): ViewModelProvider.Factory {
    return object : ViewModelProvider.Factory {
        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return factory() as T
        }
    }
}

AssistedInjectを使った実装

上記の実装もいいのですが実装量は増えますし、例えばViewModelの依存するクラスが増減した場合にはファクトリークラスの修正も必要になってしまいます。

AssitedInjectを使うとこの問題が解決できます。AssitedInjectはもともとSquareが開発しているライブラリですが、最近Dagger本体に導入されました。

github.com

dagger.dev

DaggerのAssitedInjectを使って書き換えると次のようになります(UI側の実装は変わらないので省略します)。

class BookViewModel @AssistedInject constructor(
    @Assisted private val bookId: String,
    private val bookRepository: BookRepository
) : ViewModel() {
    ... ...

    @AssistedFactory
    interface Factory {
        fun create(bookId: String): BookViewModel
    }
}

かなりシンプルになりました。AssistedInjectの詳しい使い方については上記リンクを参照してください。

UIからデータを渡さない場合

上記はUI側からViewModelコンストラクターに何かしらの値を渡すケースでしたが、値を渡さないケースも同じように実装できます。

class BookViewModel @AssistedInject constructor(
    private val bookRepository: BookRepository
) : ViewModel() {
    ... ...

    @AssistedFactory
    interface Factory {
        fun create(): BookViewModel
    }
}

SquareのAssistedInjectを利用しても同様の実装ができますが、ビルド時に以下のような警告が出ます。

Assisted injection without at least one @Assisted parameter can use @Inject", targetConstructor

一方、DaggerのAssistedInject@Assistedがないケースでの利用も想定されているようで、警告は出ません。

まとめ

  • カスタムファクトリークラスを用意してViewModelをDIする方法を紹介しました
  • Dagger2.31で導入されたAssistedInjectを使うことで実装をよりシンプルにできます
  • なお、SavedStateHandlerを扱うViewModelのDI方法については模索中です

使ったコードはこちらに置いています。

github.com