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

AACのViewModelをFactoryクラス不要で簡単に生成できる拡張関数

Androidで、MVVM及びdatabindingを行う場合、Android Architecture ComponentsのViewModelを使うことが多いと思います。
このViewModelを生成する際、コンストラクタに引数が必要だとFactoryクラスを作成する必要がありますが、ViewModelが多くなってくるとその分Factoryも増えていき、作成・管理が面倒になったので拡張関数を作成しました。

結論

忙しい人のために先に結論
Fragment.getViewModelFragment.viewModelProviderを作りました。

使い方

class SampleFragment: Fragment(), SampleNavigator {
    // フィールドでvalで持ちたい場合は、viewModelProvider
    private val viewModel by viewModelProvider {
        val str = requireNotNull(arguments?.getString(ARG_SAMPLE_STR))
        SampleViewModel(str, this)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val str= requireNotNull(arguments?.getString(ARG_SAMPLE_STR))
        // 一時的に使用したい(or lateinit varで初期化したい)場合はgetViewModel
        val vm = getViewModel { SampleViewModel(str, this) }

        // ActivityのViewModelを参照したい場合
        val viewModelOfActivity = requireActivity().getViewModel{ SampleActivityViewModel() }
    }

    // 本質外
    companion object {
        private const val ARG_SAMPLE_STR = "ARG_SAMPLE_STR"

        fun newInstance(str: String) = SampleFragment().apply {
            arguments = bundleOf(ARG_SAMPLE_STR to str)
        }
    }
}

for Fragment

FragmentExt.kt
inline fun <reified VM : ViewModel> Fragment.getViewModel(crossinline factory: () -> VM): VM {
    return ViewModelProviders.of(this, object : ViewModelProvider.Factory {
        override fun <VM : ViewModel> create(modelClass: Class<VM>): VM {
            return requireNotNull(modelClass.cast(factory()))
        }
    }).get(VM::class.java)
}

inline fun <reified VM : ViewModel> Fragment.viewModelProvider(crossinline factory: () -> VM) = lazy {
    getViewModel { factory() }
}

for FragmentActivity

上の拡張関数で生やす元がFragmentActivityになっただけ

FragmentActivityExt.kt
inline fun <reified VM : ViewModel> FragmentActivity.getViewModel(crossinline factory: () -> VM): VM {
    return ViewModelProviders.of(this, object : ViewModelProvider.Factory {
        override fun <VM : ViewModel> create(modelClass: Class<VM>): VM {
            return requireNotNull(modelClass.cast(factory()))
        }
    }).get(VM::class.java)
}

inline fun <reified VM : ViewModel> FragmentActivity.viewModelProvider(crossinline factory: () -> VM) = lazy {
    getViewModel { factory() }
}

解説

Fragment(とFragmentActivity)に拡張関数を生やして、ViewModelProviders.of().get()をいい感じにしてくれればゴールです。
ViewModelProviders.ofに必要なのは

  • Fragment
  • ViewModelProvider.Factory クラスをもらってViewModelを返すもの
  • get(ViewModelのクラス)

Fragmentはthisが使えます。
ViewModelProvider.Factoryの中身は、createを実装するもので、これはViewModelを生成して返せばゴールです。
Factoryは、引数を拡張関数は知らなくて良い(外に任せる)ので、引数無しでVMを返す高階関数で表現できます。
factory: () -> VM
getにはクラスを入れる必要があります。
拡張関数の引数でもらってきて入れるのが簡単ですが、VMはViewModelを継承したクラスと分かっているので、VM::class.javaができればクラスを引数に取る必要はありません。
そこで reifiedの登場です。

reified

3行でまとめると

reified type paramter = 具象化された型パラメータ
→reifiedを使うことで、T::class.javaを実現できる
→reifiedを使うには、inline関数である必要がある

公式サイトや太郎さんの記事が参考になります。

公式サイト
日本語
太郎さんのサイト

reifiedを使えば良いですが、このためにはinline関数にする必要があります。
ただ、crossinlineをつけないとfactory: () -> VMがcreate内から参照できません。

crossinline

まとめると

inline関数では、渡されてラムダを関数本体から直接ではなく、
ローカルオブジェクトやネストされた関数などの別の実行コンテキストから呼び出すことは許可されていない
→ラムダパラメータにcrossinlineをつけることで可能になる

詳しくは公式サイトのreifiedの上に載っています。

requireNotNull(modelClass.cast(factory()))
は警告回避のために行っています。
as TのUNCHECKED_CASTの回避のために modelClass.cast(factory())
NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONSの回避のために requireNotNull

以上を組み合わせることで拡張関数を実現しています。
調べていたとき、reifiedが便利すぎて感動しました。
T::class.javaが出来るなんてジェネリクス好き歓喜。

道行き

結論に至るまでの道のりを備忘録がてら記載。

ViewModelProvidersの利用

通常、AACのViewModelを取得するためには、ViewModelProviders.ofを利用します。

// 引数がない場合
ViewModelProviders.of(Fragment).get(SampleViewModel::class.java)
// 引数が必要な場合
ViewModelProviders.of(Fragment, SampleViewModelFactory(arg1, arg2)).get(SampleViewModel::class.java)

多くの場合、引数が必要な場合はViewModelクラスと対になるFactoryClassを作ります。

SampleViewModelFactory.kt
class SampleViewModelFactory(arg1: String, arg2: Int) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass != SampleViewModel.class.java) {
            throw IllegalArgumentException("ViewModel class is not SampleViewModel")
        }
        return TextToSpeechDemoViewModel(arg1, arg2) as T
    }
}

Util化

毎回クラスを作るのは面倒です。
引数を見てみると、必要なのはViewModelProder.Factoryとなっています。
Factoryクラスを作るときに、実装しましたね。要はこれがあればよいのです。
image.png
リファレンス

Factoryの中にはcreateメソッドがあり、中を見てみるとクラスをもらって引数を利用してViewModelを生成し、Tにキャストして返しています。
ViewModelによって必要な引数はバラバラです。
なのでViewModelを生成する処理は引数にとり外に任せることにしましょう。
ViewModelを生成する処理を高階関数 () -> Tとして引数に渡せば、生成ロジックを外に任せることができます。
これでUtilクラスで汎用的にできそうです。

ViewModelProvidersUtils.kt
// 引数がない場合の生成メソッド
fun <T : ViewModel> create(fragment: Fragment, clazz: Class<out T>): T {
    return ViewModelProviders.of(fragment, object : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return clazz.newInstance() as T
        }
    }).get(clazz)
}


// 引数がある場合の生成メソッド
fun <T : ViewModel> create(fragment: Fragment, clazz: Class<out T>, factory: () -> T): T {
    return ViewModelProviders.of(fragment, object : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return factory() as T
        }
    }).get(clazz)
}

使う側

// 引数なし
val vm = ViewModelProvidersUtils.createViewModel(SampleViewModel::class.java)
// 引数あり
val vm = ViewModelProvidersUtils.createViewModel(SampleViewModel::class.java) {
    SampleViewModel(arg1, arg2)
}

Utilの2つのメソッドの実装をよく見ると、ほとんど同じ処理をしています。
引数一つの createViewModelは、引数二つの createViewModelfactory() as Tに当たる部分でclazz.newInstance()をしていますね。
kotlinはデフォルト引数が使えるので一つにできます。

ViewModelProvidersUtils.kt
fun <T : ViewModel> create(fragment: Fragment, clazz: Class<out T>, factory: () -> T = { clazz.newInstance() as T }): T {
    return ViewModelProviders.of(fragment, object : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return factory() as T
        }
    }).get(clazz)
}

これで、
第二引数を指定しない場合はclazz.newInstance()が行われる
指定すれば引数ありの場合に対応できる
ようになりました。

拡張関数化

必ずFragmentから呼ぶもので、汎用的に使えそうなので拡張関数にしてみましょう。

FragmentExt.kt
fun <T : ViewModel> Fragment.createViewModel(clazz: Class<out T>, factory: () -> T = { clazz.newInstance() as T }): T {
    return ViewModelProviders.of(this, object : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return factory() as T
        }
    }).get(clazz)
}

使う側

Fragment内からの呼び出し
// 引数なし
val vm = createViewModel(SampleViewModel::class.java)
// 引数あり
val vm = createViewModel(SampleViewModel::class.java) {
    SampleViewModel(arg1, arg2)
}

ここで拡張関数を見てみると、(人間的には)返す型がTと決まっているので、クラスを引数に渡すのは無駄じゃない?と思います。
.get(clazz)のところをT::class.javaとできれば、clazz: Classを無くせそうです。
そこで、reifiedが登場し、最終形になります。
解説へと続くわけです

Why do not you register as a user and use Qiita more conveniently?
  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
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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