0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

hiltViewModel()でviewModelを初期化する時の落とし穴

Last updated at Posted at 2025-01-16

結論

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;
          }

    // 省略

どうやらコンストラクタ内でdelegateFactoryhiltViewModelFactoryの二種類の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の記事を投稿してみました。どなたかのお力になれたら嬉しいです。
ここまで見てくれた人、嬉しいです。ありがとうございます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?