LoginSignup
56
31

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-08-22

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:

56
31
1

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
56
31