🎃

AssistedInjectしたViewModelを共通で利用する

2021/10/11に公開

Dagger (Hilt) + ViewModelを利用していて、以下のようなユースケースに遭遇するときがあります。

FragmentAのChildにFragmentBが存在し、同じViewModelを共有したい。
そんなときは、ViewModelStoreOwnerの指定を共通にしてあげることで解決できます。
例えばActivityや親のFragmentをViewModelStoreOwnerに指定します。

@HiltViewModel
class HogeViewModel @Inject constructor() : ViewModel() {
    // ...
}
@AndroidEntryPoint
class FragmentA : Fragment() {
    private val viewModel: HogeViewModel by viewModels(
        ownerProducer = { this }, // デフォルトがthisなので必要ないですが、明示的に書いています
    )
}
@AndroidEntryPoint
class FragmentB : Fragment() {
    private val viewModel: HogeViewModel by viewModels(
        ownerProducer = { requireParentFragment() }, // FragmentAを渡す
    )
}

AssistedInjectを使いたいとき

では、上記のユースケースでViewModelにAssistedInjectしたいときはどうしたら良いでしょうか。
例として、画面遷移でidを受け取り、ViewModelに渡すケースを考えます。
idは親であるFragmentAで受け取るとすると以下のようなコードとなります。

class HogeViewmodel @AssistedInject constructor(
    @Assisted private val id: Int,
) : ViewModel() {
    @AssistedFactory
    interface Factory {
        fun create(id: Int): HogeViewModel
    }
    
    companion object {
        fun provideFactory(
	    assistedFactory: Factory,
	    id: Int,
	): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
                override fun <T : ViewModel?> create(modelClass: Class<T>): T {
		    @Suppress("UNCHECKED_CAST")
                    return assistedFactory.create(id) as T
                }
            }
    }
@AndroidEntryPoint
class FragmentA : Fragment() {
    @Inject
    lateinit var viewModelFactory: HogeViewModel.Factory
    
    private val viewModel: HogeViewModel.Factory by viewModels(
        ownerProducer = { this },
	factoryProducer = { 
	    val id = requireArguments().getInt("id")
	    HogeViewModel.provideFactory(viewModelFactory, id) 
	},
    )
}

FragmentBではどうなるか

ParentFragmentからargumentを引っ張ってきてFactoryを作ったり、どうにかしてidを取得する方法を考えるかもしれませんが、実はfactoryProducerの指定なしで取得できます。

@AndroidEntryPoint
class FragmentB : Fragment() {
    private val viewModel: HogeViewModel by viewModels(
        ownerProducer = { requireParentFragment() }, // FragmentAを渡す
	// factoryProducer = { ... }  指定する必要がない
    )
}

なぜか

順にコードを追ってみます。

viewModels()

private val viewModel: HogeViewModel by viewModels(...)

で使われているviewModels()は以下です。

@MainThread
inline fun <reified VM : ViewModel> Fragment.viewModels(
    noinline ownerProducer: () -> ViewModelStoreOwner = { this },
    noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(
        VM::class, 
        { ownerProducer().viewModelStore }, // ・・・①
        factoryProducer,
    )

@MainThread
fun <VM : ViewModel> Fragment.createViewModelLazy(
    viewModelClass: KClass<VM>,
    storeProducer: () -> ViewModelStore, // ・・・②
    factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        defaultViewModelProviderFactory
    }
    return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)

ここで注目したいのは①, ②の部分。
storeProducerにはownerProducerによって生成されるViewModelStoreOwnerviewModelStoreを渡していることが分かります。(ownerProducer().viewModelStore)
今回の例で言うと、FragmentAのviewModelStoreを渡しているということです。

ViewModelLazy

では、ViewModelLazyを見てみます。

public class ViewModelLazy<VM : ViewModel> (
    private val viewModelClass: KClass<VM>,
    private val storeProducer: () -> ViewModelStore,
    private val factoryProducer: () -> ViewModelProvider.Factory
) : Lazy<VM> {
    private var cached: VM? = null

    override val value: VM
        get() {
            val viewModel = cached
            return if (viewModel == null) {
	        // ・・・③
                val factory = factoryProducer()
                val store = storeProducer()
                ViewModelProvider(store, factory).get(viewModelClass.java).also {
		// ・・・③ここまで
                    cached = it
                }
                }
            } else {
                viewModel
            }
        }

    override fun isInitialized(): Boolean = cached != null
}

ここで注目したいのは③の部分。
先程のstoreProducerによって生成されるViewModelStorefactoryProducerによって生成されるViewModelProvider.Factoryを使ってViewModelProviderの生成およびget()メソッドによるViewModelの取得を行っています。

ViewModelProvider

ではViewModelProviderのコンストラクタとget()メソッドを見てみます。

public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
    mFactory = factory;
    mViewModelStore = store;
}
    @NonNull
    @MainThread
    public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
        String canonicalName = modelClass.getCanonicalName();
        if (canonicalName == null) {
            throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
        }
        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }
    
    @NonNull
    @MainThread
    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        // ・・・④
        ViewModel viewModel = mViewModelStore.get(key); 

        if (modelClass.isInstance(viewModel)) {
            if (mFactory instanceof OnRequeryFactory) {
                ((OnRequeryFactory) mFactory).onRequery(viewModel);
            }
            return (T) viewModel;
        } else {
            //noinspection StatementWithEmptyBody
            if (viewModel != null) {
                // TODO: log a warning.
            }
        }
	// ・・・④ここまで
	
	// ・・・⑤
        if (mFactory instanceof KeyedFactory) {
            viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
        } else {
            viewModel = mFactory.create(modelClass);
        }
        mViewModelStore.put(key, viewModel);
	// ・・・⑤ここまで
        return (T) viewModel;
    }

一旦④は飛ばして⑤を見てみます。
⑤ではコンストラクタで与えたViewModelProvider.FactoryからViewModelを生成し、mViewModelStoreに保存しています。

そして、④ではmViewModelStoreに保存されているViewModelがないか探し、あった場合はそれを取得しreturn、ない場合は⑤に続くという流れです。

つまり、mViewModelStoreにViewModelが保存されていれば、ViewModelProvider.FactoryがなくてもViewModelを取得できるということです。

今回の例でまとめると

  • FragmentAはHogeViewModel.provideFactory(viewModelFactory, id)からHogeViewModelを生成し、FragmentA.viewModelStoreに保存
  • FragmentBはFragmentA.viewModelStoreに保存されているHogeViewModelを取得

という流れになります。
ViewModelStoreOwnerが同じであれば、先にインスタンスを生成するほうのみがViewModelProvider.Factoryを渡してあげるだけで共通なViewModelを取得できるということでした。

参考

https://github.com/google/dagger/issues/2287

Discussion