Help us understand the problem. What is going on with this article?

ステップバイステップでDaggerを使った引数ありのViewModelの初期化を理解する

More than 1 year has passed since last update.

ViewModel周りの作り方、ちょっともやもやしていたので、改めてまとめてみました。

ViewModelの作り方

何も工夫せずにViewModelを取得すると以下のようになります

class SampleViewModel() : ViewModel()

// (Fragment内でのコード)
ViewModelProviders.of(this).get(SampleViewModel::class.java)

これは内部的に以下と同じです。 FragmentからviewModelStore というのを取得して、それと AndroidViewModelFactory を渡して作ります。

(Fragment内でのコード)

val factory = ViewModelProvider
        .AndroidViewModelFactory
        .getInstance(requireActivity().application)
ViewModelProvider(this.viewModelStore, factory).get(SampleViewModel::class.java)

つまり、内部的なロジックだと以下と同じで、fragment.viewModelStoreでキャッシュしていて、AndroidViewModelFactoryで作っているだけです。

(Fragment内でのコード)

private fun createViewModel(): SampleViewModel {
    val factory = ViewModelProvider
        .AndroidViewModelFactory
        .getInstance(requireActivity().application)
    // ViewModelStore内からViewModelを取得
    val existsViewModel: ViewModel? = this
        .viewModelStore
        .get(SampleViewModel::class.java.canonicalName)
    // viewModelStore内にあればそれを返す
    if (existsViewModel != null) { 
        return existsViewModel as SampleViewModel
    }
    // 無ければAndroidViewModelFactoryで作る
    return factory.create(SampleViewModel::class.java)
}

引数ありのViewModelの作り方

このFactoryを変えることができれば、引数ありで初期化できそうです。
AndroidViewModelFactoryをオーバーライドして実装すると以下のようになります。

class SampleViewModel(val sampleParameter: String) : ViewModel()

...
// Fragment内のコード
val factory = object : ViewModelProvider
  .AndroidViewModelFactory(requireActivity().application) {
    // **AndroidViewModelFactoryをオーバーライドしてカスタムしたcreateロジックを入れる**
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass == SampleViewModel::class.java) {
            // 普通にnewする!
            return SampleViewModel("sample parameter") as T
        }
        return super.create(modelClass)
    }
}
println(
    ViewModelProvider(this.viewModelStore, factory)
        .get(SampleViewModel::class.java).sampleParameter
)
I/System.out: sample parameter

DaggerでのViewModelの作り方

さて、初期化するときにRepositoryとか渡したいので、みなさんはDaggerを使いたいですよね?

class SampleViewModel @Inject constructor(val repository: SessionRepository) : ViewModel()

Fragmentに直接以下のように書くとViewModelStoreで管理されないインスタンスができてしまいます。

// ☓ ViewModelStoreに保存されていないインスタンス!!
@Inject lateinit var viewModel: SampleViewModel 

そこでDaggerのProviderというgetを呼ぶまでインスタンスが作られないものを利用します。
そして、Factoryで返させることで、ViewModelStoreで管理されるようにします。

@Inject lateinit var viewModelFactory: Provider<SampleViewModel>
val factory = object : ViewModelProvider
.AndroidViewModelFactory(requireActivity().application) {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass == SampleViewModel::class.java) {
            return viewModelFactory.get() as T
        }
        return super.create(modelClass)
    }
}
ViewModelProvider(this.viewModelStore, factory).get(SampleViewModel::class.java)

Daggerでのカスタムした引数をもつViewModelの使い方

例えばページの詳細画面などではpageIdなどを渡して、ViewModelを作りたくなりますよね?
AssistedInjectというライブラリを使うとDaggerでRepositoryとかをinjectしつつ、カスタムした引数をFactoryで渡して、カスタムした引数を渡せます。

class SampleViewModel @AssistedInject constructor(
    // カスタムした引数を使いたいものに@Assistedをつける!
    @Assisted val pageId: String,
    val repository: SessionRepository
) : ViewModel() {
    @AssistedInject.Factory
    interface Factory {
        fun create(pageId: String): SampleViewModel
    }
}
@Inject lateinit var viewModelFactory: SampleViewModel.Factory

val factory = object : ViewModelProvider
.AndroidViewModelFactory(requireActivity().application) {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass == SampleViewModel::class.java) {
            // 引数を渡して作る!
            return viewModelFactory.create("sample_page") as T
        }
        return super.create(modelClass)
    }
}
ViewModelProvider(this.viewModelStore, factory).get(SampleViewModel::class.java)

実用的なコードに落とし込む

Android Jetpackのfragment-ktxを使うと viewModels()でViewModelを初期化できます。内部的には普通にFragmentからviewModelStoreを使ったりなど、同じことをやっているだけです。

class SimpleViewModel() : ViewModel()
...
// Fragment内
val viewModel: SimpleViewModel by viewModels()

viewModelsはFactoryを渡せるので、以下のように書くことができます。

// Fragment内
val viewModel:SampleViewModel by viewModels {
        object : ViewModelProvider.AndroidViewModelFactory(this.requireActivity().application) {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                @Suppress("UNCHECKED_CAST")
                return viewModelFactory.create("sample_page") as T
            }
        }
    }

自分で viewModels を使ったextension functionを定義すると以下のようになり、かなり実用的にかけるようになりました。

class SampleViewModel @AssistedInject constructor(
    @Assisted val pageId: String,
    val repository: SessionRepository
) : ViewModel() {
    @AssistedInject.Factory
    interface Factory {
        fun create(pageId: String): SampleViewModel
    }
}

// Fragment内
@Inject lateinit var viewModelFactory: SampleViewModel.Factory
val viewModel: SampleViewModel by assistedViewModels {
    viewModelFactory.create("sample_page")
}


inline fun <reified T : ViewModel> Fragment.assistedViewModels(
    crossinline body: () -> T
): Lazy<T> {
    return viewModels {
        object : ViewModelProvider.AndroidViewModelFactory(this.requireActivity().application) {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                @Suppress("UNCHECKED_CAST")
                return body() as T
            }
        }
    }
}

まとめ

自分はなんとなく完全に理解できました。
こうしたらわかりやすいとか、ツッコミなどあれがコメントやTwitterなどでください :pray:

takahirom
Google Developers Expert for Android
cyberagent
サイバーエージェントは「21世紀を代表する会社を創る」をビジョンに掲げ、インターネットテレビ局「AbemaTV」の運営や国内トップシェアを誇るインターネット広告事業を展開しています。インターネット産業の変化に合わせ新規事業を生み出しながら事業拡大を続けています。
http://www.cyberagent.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away