コンストラクタ引数有りのViewModelを簡単に作成する
NewInstanceFactoryを継承したFactoryをいちいち用意しないようにする一方法
Created at Updated at

1526 Words
⚠️

補足 (2021-02-20)

この記事の内容は Dagger + Hilt を使用したDI を行う場合は関係ない内容になっています。
DIライブラリを使用せずViewModelへのコンストラクタインジェクションだけを軽く行いたい場合(限定的すぎる?)などにちょっと楽をする方法、くらいに見てもらればと思います。


追記 (2020-12-27)

ViewModelOwner#lazyProvideViewModelを追加。


前提1

ViewModelの最もシンプルな作り方は多分こんな感じだろう。

class HogeViewModel : ViewModel() {
    // LiveDataなど色々
}
class HogeActivity {
    private val viewModel : HogeViewModel by lazy {
        ViewModelProvider(this)[HogeViewModel::class.java]
    }
    // 他省略
}

ViewModelProviderViewModelStoreOwnerViewModelProvider.Factoryを引数にとり、後者を省略すると無引数コンストラクタでViewModelが生成される。
後半の[]の中身は、(省略可能な)キー名生成するViewModelのクラスを指定する。キーownerが同型のViewModelを複数持つ場合にそれぞれを識別するために使う。

前提2

以上の方法ではViewModelが引数有りコンストラクタを用意することができないため、外部から初期値を与えたい場合などは対象のプロパティをミュータブルにせざるを得ない問題がある。
きっちりイミュータブルにしておきたい場合は、次のようにインスタンス生成用のFactoryを用意する。

class HogeViewModel(
    private val hogeArg : Hoge
) : ViewModel() {
    // ------ //
    class Factory(
        private val hogeArg : Hoge
    ) : ViewModelProvider.NewInstanceFactory() {
        @Suppress("unchecked_cast")
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            return HogeViewModel(hogeArg) as T
        }
    }
}
class HogeActivity {
    private val viewModel : HogeViewModel by lazy {
        val hoge = Hoge()
        val factory = HogeViewModel.Factory(hoge)
        ViewModelProvider(this, factory)[HogeViewModel::class.java]
    }
    // 他省略
}

HogeActivity側でHogeViewModel生成時にFactoryを渡すことで、Factory#create()メソッドによってインスタンスが生成される。

簡単化

新しいViewModelを作るごとにこのFactoryを毎回書くのは少々面倒であるし無駄に冗長なので、Factoryを作る関数と、作ったFactoryを使ってViewModelインスタンスを生成する関数を作って簡単にする。

package com.suihan74.utilities

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import kotlin.reflect.KClass

class ViewModelFactory<ViewModelT : ViewModel>(
    private val creator: () -> ViewModelT,
    private val kClass: KClass<ViewModelT>
) : ViewModelProvider.NewInstanceFactory() {

    @Suppress("unchecked_cast")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return creator.invoke() as T
    }

    /** ViewModelを作成・取得する */
    fun provide(owner: ViewModelStoreOwner, key: String? = null) : ViewModelT =
        if (key == null) ViewModelProvider(owner, this)[kClass.java]
        else ViewModelProvider(owner, this)[key, kClass.java]
}

/**
 * ViewModel作成時に用いるNewInstanceFactoryを生成する
 */
inline fun <reified ViewModelT : ViewModel> createViewModelFactory(
    noinline creator: ()->ViewModelT
) = ViewModelFactory(creator, ViewModelT::class)

/**
 * ViewModelを作成・取得する
 */
inline fun <reified ViewModelT : ViewModel> provideViewModel(
    owner: ViewModelStoreOwner,
    noinline creator: ()->ViewModelT
) = createViewModelFactory(creator).provide(owner)

/**
 * ViewModelを作成・取得する
 */
inline fun <reified ViewModelT : ViewModel> provideViewModel(
    owner: ViewModelStoreOwner,
    key: String?,
    noinline creator: ()->ViewModelT
) = createViewModelFactory(creator).provide(owner, key)

// ------ //

/**
 * ViewModelを作成・取得する(lazy版)
 */
inline fun <reified ViewModelT : ViewModel> ViewModelStoreOwner.lazyProvideViewModel(
    noinline creator: ()->ViewModelT
) = lazy { provideViewModel(this, creator) }

/**
 * ViewModelを作成・取得する(lazy版)
 */
inline fun <reified ViewModelT : ViewModel> ViewModelStoreOwner.lazyProvideViewModel(
    key: String?,
    noinline creator: ()->ViewModelT
) = lazy { provideViewModel(this, key, creator) }

以上の内容のファイルを用意しておいて、次のように使う。

class HogeViewModel(
    private val hoge: Hoge
) : ViewModel() {
    // 省略
}
class HogeActivity {
    private val viewModel : HogeViewModel by lazy {
        provideViewModel(this) {
            val hoge = Hoge()
            HogeViewModel(hoge)
        }
    }
    // 他省略
}

キーを使う場合はprovideViewModel(this, key) { // --- // }とする。

これで少しはすっきりしたというものです。
よかったですね。

lazyProvideViewModelを追加 (2020-12-27)

private val viewModel by lazy {
    provideViewModel(this) {
        HogeViewModel()
    }
}

をもう少し簡単に書くためにlazyProvideViewModel関数を追加した。

private val viewModel by lazyProvideViewModel {
    HogeViewModel()
}

使用できるViewModelStoreOwnerthis限定である(他のownerを渡せない)という制限がある点には注意。
これはFragmentのプロパティ初期化などでActivityなどをownerに指定するとFragmentインスタンス生成時にはまだアタッチ前なので失敗する問題があるため、このようにしている。

See Also