viewModel()とhiltViewModel()の比較

2024/07/29に公開

結論

Dagger Hiltが入っているプロジェクトならhiltViewModelがオススメ

引数がない場合

ほぼ変わらない

引数があるがHiltで解決できる場合

hiltViewModelであれば引数を渡さなくても依存解決してくれるため行数が短くなる

引数がありAssisted Injectする必要がある場合

viewModelhiltViewModelで引数の型が変わる・・・がhiltViewModelの方が簡潔になりやすい印象

コード上の違い

hiltViewModel()は内部でviewModel()を呼び出している

@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)
}

@Composable
inline fun <reified VM : ViewModel, reified VMF> hiltViewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null,
    noinline creationCallback: (VMF) -> VM
): VM {
    val factory = createHiltViewModelFactory(viewModelStoreOwner)
    return viewModel(
        viewModelStoreOwner = viewModelStoreOwner,
        key = key,
        factory = factory,
        extras = viewModelStoreOwner.run {
            if (this is HasDefaultViewModelProviderFactory) {
                this.defaultViewModelCreationExtras.withCreationCallback(creationCallback)
            } else {
                CreationExtras.Empty.withCreationCallback(creationCallback)
            }
        }
    )
}

viewModelの実装は以下の通り

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.java, viewModelStoreOwner, key, factory, extras)

コード上のviewModelhiltViewModelの違いは

  1. factoryを
  • viewModelStoreOwnerから生成するか(hiltViewModel)
  • 直接渡すか(viewModel)
  1. extrasを
  • ViewModelFactory → ViewModel関数から生成するか(hiltViewModel)
  • 直接渡すか(viewModel)

と考えることが可能

1. factory

createHiltViewModelFactory(viewModelStoreOwner) を見てみる

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

ViewModelStoreOwnerの値によって変わる・・・が基本上の方を通ると思われる(例えばNavBackStackEntryも継承している)

defaultViewModelProviderFactoryはデフォルトのFactoryとのこと
特にFactoryを指定されていないときはこれを使うらしい?

/**
 * Returns the default [ViewModelProvider.Factory] that should be
 * used when no custom `Factory` is provided to the
 * [ViewModelProvider] constructors.
 */
val defaultViewModelProviderFactory: ViewModelProvider.Factory

次にHiltViewModelFactoryを見てみる

@JvmName("create")
public fun HiltViewModelFactory(
    context: Context,
    delegateFactory: ViewModelProvider.Factory
): ViewModelProvider.Factory {
    val activity = context.let {
        var ctx = it
        while (ctx is ContextWrapper) {
            // Hilt can only be used with ComponentActivity
            if (ctx is ComponentActivity) {
                return@let ctx
            }
            ctx = ctx.baseContext
        }
        throw IllegalStateException(
            "Expected an activity context for creating a HiltViewModelFactory " +
                "but instead found: $ctx"
        )
    }
    return HiltViewModelFactory.createInternal(
        /* activity = */ activity,
        /* delegateFactory = */ delegateFactory
    )
}

呼び出し元のActivityインスタンスを取得して次に進んでいる

ここでHiltViewModelFactory.createInternalは、Dagger Hiltライブラリ内に定義されているHiltViewModelFactoryというFactoryクラスのインスタンスを取ってくる処理である
このFactoryにはDagger Hiltで登録されているViewModelの依存関係の解決を行ってくれる関数が生えており、Dagger Hilt側で依存解決できるものに関しても同時に解決してくれるようになっている

まとめると

  • hiltViewModelの場合、DaggerHiltで登録されたViewModelを生成してくれるHiltViewModelFactoryクラスが設定される
    • ViewModelの引数の中にDagger Hilt側で依存解決ができるものがあれば解決を行ってくれる
    • 登録されてなければデフォルトのFactoryクラスを利用する
  • viewModelの場合、自分でFactoryの取得、解決をする必要がある

2. extras

そもそもextrasとは、(ViewModelFactory) → ViewModelを保存しているMapのこと

/**
 * Simple map-like object that passed in [ViewModelProvider.Factory.create]
 * to provide an additional information to a factory.
 *
 * It allows making `Factory` implementations stateless, which makes an injection of factories
 * easier because  don't require all information be available at construction time.
 */
public abstract class CreationExtras internal constructor() {
    internal val map: MutableMap<Key<*>, Any?> = mutableMapOf()

    /**
     * Key for the elements of [CreationExtras]. [T] is a type of an element with this key.
     */
    public interface Key<T>

    /**
     * Returns an element associated with the given [key]
     */
    public abstract operator fun <T> get(key: Key<T>): T?

    /**
     * Empty [CreationExtras]
     */
    object Empty : CreationExtras() {
        override fun <T> get(key: Key<T>): T? = null
    }
}

つまりここから対応する(ViewModelFactory) → ViewModelを探し出してきて適用する挙動をとると思われる

これを踏まえてhiltViewModelviewModelの差分を見てみる

extras = viewModelStoreOwner.run {
  if (this is HasDefaultViewModelProviderFactory) {
    this.defaultViewModelCreationExtras.withCreationCallback(creationCallback)
  } else {
    CreationExtras.Empty.withCreationCallback(creationCallback)
  }
}

ここで重要と思われるのはwithCreationCallback(creationCallback)という記述

creationCallbackというのはhiltViewModelの引数にある、(ViewModelFactory) → ViewModel

@Composable
inline fun <reified VM : ViewModel, reified VMF> hiltViewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null,
    noinline creationCallback: (VMF) -> VM
)@Composable
inline fun <reified VM : ViewModel, reified VMF> hiltViewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null,
    noinline creationCallback: (VMF) -> VM
)

withCreationCallback(creationCallback)は、extrasにcreationCallbackを追加する挙動を行う

/**
 * Returns a new {@code CreationExtras} with the original entries plus the passed in creation
 * callback. The callback is used by Hilt to create {@link AssistedInject}-annotated {@link
 * HiltViewModel}s.
 *
 * @param callback A creation callback that takes an assisted factory and returns a {@code
 *   ViewModel}.
 */
fun <VMF> CreationExtras.withCreationCallback(callback: (VMF) -> ViewModel): CreationExtras =
  MutableCreationExtras(this).addCreationCallback(callback)

/**
 * Returns the {@code MutableCreationExtras} with the passed in creation callback added. The
 * callback is used by Hilt to create {@link AssistedInject}-annotated {@link HiltViewModel}s.
 *
 * @param callback A creation callback that takes an assisted factory and returns a {@code
 *   ViewModel}.
 */
@Suppress("UNCHECKED_CAST")
fun <VMF> MutableCreationExtras.addCreationCallback(callback: (VMF) -> ViewModel): CreationExtras =
  this.apply {
    this[HiltViewModelFactory.CREATION_CALLBACK_KEY] = { factory -> callback(factory as VMF) }
  }

まとめると、デフォルトのextrasにユーザーが指定したextraを追加したものを使っていることになる(=対象のViewModelextraだけを渡せばいい)

対してviewModelではextrasを渡す必要があり、少し面倒(default + 対象のViewModelextraみたいなコードを書く必要がある)

結論

差分は以下の通り

viewModel

  • 渡されたfactoryextrasの値を元にViewModelを生成して返す
  • 引数のないViewModelなどはfactoryextrasがそもそもいらないのでデフォルト引数のままでいい

hiltViewModel

  • Dagger Hiltの機能を存分に使ったviewModelの糖衣構文的な関数
  • FactoryはDagger Hiltの機能を用いて勝手に解決してくれる
  • extraが必要な場合、対象となるViewModelextraを用意するだけでよい

これらの性質を踏まえると

  • 引数が全くない時はほぼ変わらない
    • viewModelの方が僅かにパフォーマンスは良い(hilt側のコードを呼ぶ必要がなくなるため)
    • R8挟んだら差はなくなる可能性もある
  • 使いこごちの差は引数がある時に顕著になる
    • viewModelの場合、factoryextrasを書く必要がある
    • hiltViewModelの場合、Dagger Hilt側で依存解決が出来る場合依存解決を行う
    • Assisted Injectを使うときもfactoryに関しては依存解決してくれる(@AssitedFactoryとして定義する必要はある)
  • Hilt × Composeの場合はhiltViewModelを使うとコードがスッキリして良さそう

余談

  • ActivityFragmentで使えるhiltViewModelsがあってもいい気がする(あるのかも?)
  • 自作ライブラリを作るために調査
    • 適当な調査だしライブラリ自体も使い物になるのか怪しいが、こういう風に書けたら便利だと思ってる

Discussion