AssistedInjectしたViewModelを共通で利用する
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によって生成されるViewModelStoreOwnerのviewModelStoreを渡していることが分かります。(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によって生成されるViewModelStoreとfactoryProducerによって生成される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を取得できるということでした。
参考
Discussion