結論
viewModelをhiltViewModelで初期化すると、intentで渡したパラメータをSavedStateHandleで受け取れない!!!!!
※間違ってたらコメント頂けると嬉しいです!🙇♂️
背景
viewModelを初期化したと同時にSavedStateHandle経由で値を取得したいことがままあると思います。
例えば、下記のようにinitした時にActivityからintent経由でもらった値を使用したい時などです。
ViewModel
@HiltViewModel
class HogeViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val hogeRepository: HogeRepository,
) : ViewModel() {
object Contract {
val key: String = "params"
}
private val hogeParams = savedStateHandle.get<HogeParameter>(Contract.key) ?: HogeParameter()
init {
Log.d("Hoge", "hogeParams: $hogeParams")
}
}
Intentを発行するActivity
@AndroidEntryPoint
class HogeActivity : AppCompatActivity() {
companion object {
fun createIntent(
context: Context,
hogeParameter: HogeParameter,
): Intent {
return Intent(context, HogeActivity::class.java).apply {
putExtras(
bundleOf(
HogeViewModel.Contract.key to hogeParameter,
)
)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HogeTheme {
HogeScreen()
}
}
}
}
viewModelをhiltViewModel()で初期化するScreen
@Composable
fun HogeScreen(
modifier: Modifier = Modifier,
hogeViewModel: HogeViewModel = hiltViewModel(),
) {
// 省略
}
一見、問題なくviewModelでHogeParameterを取得できそうな実装ですが、残念ながらHogeViewModel内のLogはHogeParameterの中身に関係なく、nullと表示されてしまいます。
ちなみにhiltのバージョンは"2.53.1"です。
対応方法
下記のようにhiltViewModel()ではなく、composeもしくはComponentActivityのviewModelで初期化すると解決します。
@AndroidEntryPoint
class HogeActivity : AppCompatActivity() {
// 省略
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// composeで初期化
val viewModel = viewModel<HogeViewModel>()
// activityで初期化する場合はこちら
// val viewModel : HogeViewModel by viewModels()
HogeTheme {
HogeScreen(hogeViewModel = viewModel)
}
}
}
}
なぜ?
hiltViewModel()とcomposeのviewModel<>()では、使用するviewModelFactoryが異なっているためでした。それぞれコードを追っていきましょう。
composeのviewModel<>()の場合
@Suppress("MissingJvmstatic")
@Composable
public inline fun <reified VM : ViewModel> viewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null,
factory: ViewModelProvider.Factory? = null,
extras: CreationExtras = if (viewModelStoreOwner is HasDefaultViewModelProviderFactory) {
viewModelStoreOwner.defaultViewModelCreationExtras
} else {
CreationExtras.Empty
}
): VM = viewModel(VM::class, viewModelStoreOwner, key, factory, extras)
ポイントはfactoryがnullで渡っていることです。
hiltViewModel()の場合
@Composable
inline fun <reified VM : ViewModel> hiltViewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null
): VM {
val factory = createHiltViewModelFactory(viewModelStoreOwner)
return viewModel(viewModelStoreOwner, key, factory = factory)
}
hiltViewModelの場合は、どうやら独自でfactoryを生成していることがわかります。
両者のコードを追っていくと、どちらも下記のコードに行き着きます。
internal fun <VM : ViewModel> ViewModelStoreOwner.get(
modelClass: KClass<VM>,
key: String? = null,
factory: ViewModelProvider.Factory? = null,
extras: CreationExtras = if (this is HasDefaultViewModelProviderFactory) {
this.defaultViewModelCreationExtras
} else {
CreationExtras.Empty
}
): VM {
val provider = if (factory != null) {
ViewModelProvider.create(this.viewModelStore, factory, extras) // hiltViewModel()だとここを通る
} else if (this is HasDefaultViewModelProviderFactory) {
ViewModelProvider.create(this.viewModelStore, this.defaultViewModelProviderFactory, extras) // composeのviewModel<>()だとここを通る
} else {
ViewModelProvider.create(this)
}
return if (key != null) {
provider[key, modelClass]
} else {
provider[modelClass]
}
}
hiltViewModelの場合は、独自でfactoryを生成しているため、ViewModelProvider.create(this.viewModelStore, factory, extras)
を返します。
一方で、composeのviewModel<>()の場合は、factoryがnullのため、ViewModelProvider.create(this.viewModelStore, this.defaultViewModelProviderFactory, extras)
を返します。
factoryがnullの場合にはdefaultViewModelProviderFactory
を使用しているようです。
中身はインターフェースになっていて、実行部分を見ると、@AndroidEntoryPoint
を設定したActivityのジェネレータ?内のデフォルトのviewModelFactoryを返すメソッドに辿り着きました。
@Override
public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
return DefaultViewModelFactories.getActivityFactory(this, super.getDefaultViewModelProviderFactory());
}
さらにsuper.getDefaultViewModelProviderFactory()
の中身を見ると、
override val defaultViewModelProviderFactory: ViewModelProvider.Factory by lazy {
SavedStateViewModelFactory(
application,
this,
if (intent != null) intent.extras else null
)
}
viewModelFactory生成時にintentの中身を渡していました。
だから、SavedStateHadleからintentの中身を取得できていたのですね。ふむふむ納得。
では、本題のhiltViewModel()のviewModelFactoryはどうなのでしょうか?
@Composable
inline fun <reified VM : ViewModel> hiltViewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null
): VM {
val factory = createHiltViewModelFactory(viewModelStoreOwner)
return viewModel(viewModelStoreOwner, key, factory = factory)
}
再度登場したこちらのコードから、今度はcreateHiltViewModelFactory
の中身を見ていきましょう。
@Composable
@PublishedApi
internal fun createHiltViewModelFactory(
viewModelStoreOwner: ViewModelStoreOwner
): ViewModelProvider.Factory? = if (viewModelStoreOwner is HasDefaultViewModelProviderFactory) {
HiltViewModelFactory(
context = LocalContext.current,
delegateFactory = viewModelStoreOwner.defaultViewModelProviderFactory
)
} else {
// Use the default factory provided by the ViewModelStoreOwner
// and assume it is an @AndroidEntryPoint annotated fragment or activity
null
}
デバックするとわかるのですが、hiltViewModel()でviewModelを初期化すると、HiltViewModelFactory
を返すようになっています。
そしてまた登場しました。defaultViewModelProviderFactory
です。
・・・あれ?
defaultViewModelProviderFactory使ってfactory生成しているなら、このままdelegateFactory
を使えばcomposeのviewModel<>()と同じ結果になりそうだけど。。。
さらにHiltViewModelFactory
の中身コードを追っていくと、Hilt内でfactoryを生成している下記のコードに辿り着くと思います。
public final class HiltViewModelFactory implements ViewModelProvider.Factory {
// 省略
public HiltViewModelFactory(
@NonNull Map<Class<?>, Boolean> hiltViewModelKeys,
@NonNull ViewModelProvider.Factory delegateFactory,
@NonNull ViewModelComponentBuilder viewModelComponentBuilder) {
this.hiltViewModelKeys = hiltViewModelKeys;
this.delegateFactory = delegateFactory;
this.hiltViewModelFactory =
new ViewModelProvider.Factory() {
@NonNull
@Override
public <T extends ViewModel> T create(
@NonNull Class<T> modelClass, @NonNull CreationExtras extras) {
RetainedLifecycleImpl lifecycle = new RetainedLifecycleImpl();
ViewModelComponent component =
viewModelComponentBuilder
.savedStateHandle(createSavedStateHandle(extras))
.viewModelLifecycle(lifecycle)
.build();
T viewModel = createViewModel(component, modelClass, extras);
viewModel.addCloseable(lifecycle::dispatchOnCleared);
return viewModel;
}
// 省略
どうやらコンストラクタ内でdelegateFactory
とhiltViewModelFactory
の二種類のfactoryが作られていることがわかりました。
そしてここで唐突に答え合わせです。
このクラスはFactoryをimplementsしているため、createをオーバーライドしています。
中身を見ると、
@NonNull
@Override
public <T extends ViewModel> T create(
@NonNull Class<T> modelClass, @NonNull CreationExtras extras) {
if (hiltViewModelKeys.containsKey(modelClass)) {
return hiltViewModelFactory.create(modelClass, extras);
} else {
return delegateFactory.create(modelClass, extras);
}
}
hiltViewModelKeysがmodelClassを含んでいるとき、hiltViewModelはdelegateFactoryを返しません。
つまり、先ほど登場したintentで渡した値をSavedStateHandleに渡してくれる下記のfactoryを使っていないのです。
override val defaultViewModelProviderFactory: ViewModelProvider.Factory by lazy {
SavedStateViewModelFactory(
application,
this,
if (intent != null) intent.extras else null
)
}
長かったですが、ここまででhiltViewModel()とcomposeのviewModel<>()では、(条件次第ではあるが)viewModelを初期化する時に使用するfactoryが異なっていることがわかりました。
しかしながら、ここまでだと使ってるfactoryが違うことまでは説明できますが、hiltViewModelFactory
でintentの中身が取得できていない理由にはなりません。
実際、hiltViewModelFactory
の方でも、createする時に.savedStateHandle(createSavedStateHandle(extras))
しているため、SavedStateHadle自体は渡っていそうです。
つまり、憶測にはなってしまいますが、hiltViewModelFactory
ではSavedStateHadle自体は渡しているものの、その中身にはintentが含まれていないということなのだと思います。
(何回も言いますが、間違っていたら教えてください!)
感想
もう少し調べた方が良さそうなのですが、ハマりそうなので一旦ここらでアプトプットとさせてください。
初めてQuitaの記事を投稿してみました。どなたかのお力になれたら嬉しいです。
ここまで見てくれた人、嬉しいです。ありがとうございます。