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