まとめ
- ナビゲーショングラフ内で
viewModel()
を使用した場合、取得できるViewModel
はActivity
やFragment
のそれとは別の、独自のViewModelStoreOwner
に紐づいています。 - そのナビゲーションの乗っている
Activity
やFragment
に紐づいたものは取得できません。 - 特に
AndroidView
と組み合わせて使用した場合、Activity
やFragment
のライフライクルに紐づいたものを取得できることを無意識のうちに期待しがちなので注意。 - ナビゲーショングラフはデスティネーション毎(パス毎)に独立した
ViewModelStoreOwner
を持ちます。
つまりこういうこと
以下のようなViewModel
クラスとActivity
があった時に、
class SampleViewModel : ViewModel() {
// 新しいインスタンスができるたびにユニークなIDを振ります。
private val id = UUID.randomUUID()
fun getId() = id
}
class MainActivity : ComponentActivity() {
private val viewModel: SampleViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Log.i(TAG, "setContent viewModel id : " + viewModel.getId())
MakeTransparentAppTheme {
MakeNavigation()
}
}
}
}
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
private fun MakeNavigation(viewModel: SampleViewModel = viewModel()) {
Log.i(TAG, "MakeNavigation viewModel id : " + viewModel.getId())
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("home") {
val viewModel1: SampleViewModel = viewModel()
Log.i(TAG, "NavStackEntry Home1 viewModel id : " + viewModel1.getId())
Home1()
}
composable("home2") {
val viewModel2: SampleViewModel = viewModel()
Log.i(TAG, "NavStackEntry Home1 viewModel id : " + viewModel2.getId())
Home2()
}
}
rememberCoroutineScope().launch {
delay(1000)
Log.i(TAG, "navigate to Home2")
navController.navigate("home2")
}
}
@Composable
fun Home1(viewModel: SampleViewModel = viewModel()) {
Log.i(TAG, "Home1 viewModel id : " + viewModel.getId())
Home2()
}
@Composable
fun Home2(viewModel: SampleViewModel = viewModel()) {
Log.i(TAG, "Home2 viewModel id : " + viewModel.getId())
Home3()
}
@Composable
fun Home3(viewModel: SampleViewModel = viewModel()) {
Log.i(TAG, "Home3 viewModel id : " + viewModel.getId())
}
Logcatはこのようになります。(抜粋)
setContent viewModel id : 4971f3d3-bfc0-4775-b8b0-7180ab8759d1 <-- ActivityのViewModelStoreOwnerに紐づいているViewModel
MakeNavigation viewModel id : 4971f3d3-bfc0-4775-b8b0-7180ab8759d1 <-- ActivityのViewModelStoreOwnerに紐づいているViewModel
// ここから"home"パスを表示
NavHost is called
NavStackEntry Home1 viewModel id : aaebbc8a-759c-4885-8524-ffb07136c917 <-- (Activityではなく)独自のViewModelStoreOwnerに紐づいているViewModel="home"パスのNavStackEntryのViewModelStoreOwnerに紐づいているViewModel
Home1 viewModel id : aaebbc8a-759c-4885-8524-ffb07136c917 <-- "home"パスのNavStackEntryと同じ独自のViewModelStoreOwnerに紐づいているViewModel
Home2 viewModel id : aaebbc8a-759c-4885-8524-ffb07136c917 <-- "home"パスのNavStackEntryと同じ独自のViewModelStoreOwnerに紐づいているViewModel
Home3 viewModel id : aaebbc8a-759c-4885-8524-ffb07136c917 <-- "home"パスのNavStackEntryと同じ独自のViewModelStoreOwnerに紐づいているViewModel
// ここから"home2"パスへ移動処理
navigate to Home2
NavStackEntry Home1 viewModel id : aaebbc8a-759c-4885-8524-ffb07136c917 <-- "home"パスのNavStackEntryと同じ独自のViewModelStoreOwnerに紐づいているViewModel="home2"パスのNavStackEntryのViewModelStoreOwnerに紐づいているViewModel
NavStackEntry Home2 viewModel id : e38c3070-860a-4f40-bdca-37b6b65ec999 <-- Activityとも"home"のパスの時とも違う独自のViewModelStoreOwnerに紐づいているViewModel
Home2 viewModel id : e38c3070-860a-4f40-bdca-37b6b65ec999 <-- "home2"パスのNavStackEntryと同じ独自のViewModelStoreOwnerに紐づいているViewModel
Home3 viewModel id : e38c3070-860a-4f40-bdca-37b6b65ec999 <-- "home2"パスのNavStackEntryと同じ独自のViewModelStoreOwnerに紐づいているViewModel
NavStackEntry Home2 viewModel id : e38c3070-860a-4f40-bdca-37b6b65ec999 <-- "home2"パスのNavStackEntryと同じ独自のViewModelStoreOwnerに紐づいているViewModel
細かく
上記の通りで、またナビゲーションが画面から画面への遷移を表現するために使われることが多いことを考えれば納得の動きです。
ですが最初この動きを把握しておらず、どうなっているのかコードを追ってみて理解しました。
せっかくコードを追ったので備忘録として要点をかいつまんでですが残しておきます。
環境
- Jetpack Compose ライブラリ : v1.2.0
コンポーズ可能なメソッド内でviewModel()を呼んだ時何が起きるのか
コンポーズ可能なメソッド内でviewModel()を呼んだ時は以下のようなメソッドが呼ばれています。
(コード内のXXXで始まるコメント文は私が足したものです。)
@Suppress("MissingJvmstatic")
@Composable
public inline fun <reified VM : ViewModel> viewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}, // XXX 引数なしでviewModel()を呼ぶと、viewModelStoreOwnerとして
// LocalViewModelStoreOwner.currentが(Nullでなければ)使われます。
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)
第一引数の、
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
のあたりで、viewModelStoreOwner
としてLocalViewModelStoreOwner.current
が使われています。(…①)
ナビゲーショングラフ内で何をしているか
ちょっと話は飛びますが、次はコンポーズ可能なメソッドの中でNavHost
メソッドを呼んだ時の動きについて追います。
コンポーズ可能なメソッドの中でNavHost
メソッドを呼んだ時は以下のような定義のメソッドが呼ばれます。
@Composable
public fun NavHost(
navController: NavHostController,
startDestination: String,
modifier: Modifier = Modifier,
route: String? = null,
builder: NavGraphBuilder.() -> Unit
) {
NavHost(
navController,
remember(route, startDestination, builder) {
navController.createGraph(startDestination, route, builder)
},
modifier
)
}
この中で呼ばれているNavHost
メソッドは以下のような定義になっています。
(コード内のXXXで始まる日本語のコメント文は私が足したものです。)
@Composable
public fun NavHost(
navController: NavHostController,
graph: NavGraph,
modifier: Modifier = Modifier
) {
val lifecycleOwner = LocalLifecycleOwner.current
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner"
} // XXX ここでもviewModelStoreOwnerとしてLocalViewModelStoreOwner.currentが使われています。
val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
val onBackPressedDispatcher = onBackPressedDispatcherOwner?.onBackPressedDispatcher
// Setup the navController with proper owners
navController.setLifecycleOwner(lifecycleOwner)
navController.setViewModelStore(viewModelStoreOwner.viewModelStore) // XXX navControllerのViewModelStoreは
// LocalViewModelStoreOwner.current.viewModelStoreになりました。
// (中略)
val visibleEntries by remember(navController.visibleEntries) {
navController.visibleEntries.map {
it.filter {
entry -> entry.destination.navigatorName == ComposeNavigator.NAME
}
}
}.collectAsState(emptyList()) // XXX navController.visibleEntriesの値(のうちCompose用のもの)をStateにして
// visibleEntriesとして持ちます。
val backStackEntry = visibleEntries.lastOrNull() // XXX visibleEntriesの最後のアイテムを
// backStackEntryとしています。
var initialCrossfade by remember { mutableStateOf(true) }
if (backStackEntry != null) {
// while in the scope of the composable, we provide the navBackStackEntry as the
// ViewModelStoreOwner and LifecycleOwner
Crossfade(backStackEntry.id, modifier) {
val lastEntry = visibleEntries.last { entry ->
it == entry.id
}// XXX visibleEntriesの最後のアイテムを
// 今度はlastEntryとしています。
// We are disposing on a Unit as we only want to dispose when the CrossFade completes
DisposableEffect(Unit) {
if (initialCrossfade) {
// There's no animation for the initial crossfade,
// so we can instantly mark the transition as complete
visibleEntries.forEach { entry ->
composeNavigator.onTransitionComplete(entry)
}
initialCrossfade = false
}
onDispose {
visibleEntries.forEach { entry ->
composeNavigator.onTransitionComplete(entry)
}
}
}
lastEntry.LocalOwnersProvider(saveableStateHolder) {
(lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
} // XXX lastEntry#.LocalOwnersProviderメソッドを呼んでいます。
}
}
// (以下略)
}
ここでの処理の大事なところは、最後の
lastEntry.LocalOwnersProvider(saveableStateHolder) {
(lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
} // XXX lastEntry#.LocalOwnersProviderメソッドを呼んでいます。
のlastEntry.LocalOwnersProvider
部分です。
このlastEntry
は少し前の
val lastEntry = visibleEntries.last { entry ->
it == entry.id
}// XXX visibleEntriesの最後のアイテムを
// 今度はlastEntryとしています。
よりvisibleEntries
の最後のアイテムになっており、
visibleEntries
はさらに少し前の方((中略)
の直後)の
val visibleEntries by remember(navController.visibleEntries) {
navController.visibleEntries.map {
it.filter {
entry -> entry.destination.navigatorName == ComposeNavigator.NAME
}
}
}.collectAsState(emptyList()) // XXX navController.visibleEntriesの値(のうちCompose用のもの)をStateにして
// visibleEntriesとして持ちます。
より、navController.visibleEntriesの値(のうちCompose用のもの)
をState
としたものです。
ちなみにnavController.visibleEntries
はNavBackStackEntry
のリストのStateFlowで、現在表示しているパスの画面やバックスタックに積まれているものが変わると変化します。
→つまりNavController.navigate
やNavController.popBackStack
メソッドが呼ばれると変化します。
よって、navController.visibleEntries
の(最後の?)値が変わるたびに、自動的に、先ほど「ここでの処理の大事なところ」とした
lastEntry.LocalOwnersProvider(saveableStateHolder) {
(lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
} // XXX lastEntry#.LocalOwnersProviderメソッドを呼んでいます。
が呼ばれます。
さて、ではlastEntry.LocalOwnersProvider
メソッドが何をしているかですが、このメソッドの定義は以下です。
※ちなみにNavBackStackEntry
クラスはViewModelStoreOwner
も実装しています。
@Composable
public fun NavBackStackEntry.LocalOwnersProvider(
saveableStateHolder: SaveableStateHolder,
content: @Composable () -> Unit
) {
CompositionLocalProvider(
LocalViewModelStoreOwner provides this,
LocalLifecycleOwner provides this,
LocalSavedStateRegistryOwner provides this
) {
saveableStateHolder.SaveableStateProvider(content)
}
}
最初に注目すべきはCompositionLocalProvider
メソッドの第一引数のLocalViewModelStoreOwner provides this
です。
こちらのメソッドの定義は
public object LocalViewModelStoreOwner {
private val LocalViewModelStoreOwner =
compositionLocalOf<ViewModelStoreOwner?> { null }
/**
* Returns current composition local value for the owner or `null` if one has not
* been provided nor is one available via [ViewTreeViewModelStoreOwner.get] on the
* current [LocalView].
*/
public val current: ViewModelStoreOwner?
@Composable
get() = LocalViewModelStoreOwner.current
?: ViewTreeViewModelStoreOwner.get(LocalView.current)
/**
* Associates a [LocalViewModelStoreOwner] key to a value in a call to
* [CompositionLocalProvider].
*/
public infix fun provides(viewModelStoreOwner: ViewModelStoreOwner):
ProvidedValue<ViewModelStoreOwner?> {
return LocalViewModelStoreOwner.provides(viewModelStoreOwner)
}
}
の最後のpublic infix fun provides
メソッドです。(ちなみに、余談&捕捉ですがinfix
メソッドは呼び出し時にメソッド名と引数の間のカッコ(
)
を省略して書けるメソッドです。)
こちらは何をしているかというと引数で渡されたviewModelStoreOwner
を引数にして、LocalViewModelStoreOwner.provides
メソッドを呼んでいます。この、「引数で渡されたviewMOdelStoreOwner
」とは今回の場合lastEntry
(=「現在表示している画面=NavBackStackEntry
)」です。
LocalViewModelStoreOwner.provides
メソッドの定義は以下で
@Suppress("UNCHECKED_CAST")
infix fun provides(value: T) = ProvidedValue(this, value, true)
ProvidedValue
クラスの定義は以下です。
要するに「第1フィールドとしてLocalViewModelStoreOwner
、第2フィールドとして現在表示しているNavBackStackEntry
が入っているdataクラスのようなもの」と覚えておけばいいと思います。
class ProvidedValue<T> internal constructor(
val compositionLocal: CompositionLocal<T>,
val value: T,
val canOverride: Boolean
)
さて、public fun NavBackStackEntry.LocalOwnersProvider
の処理の中身追いに戻ります。ここまでで
@Composable
public fun NavBackStackEntry.LocalOwnersProvider(
saveableStateHolder: SaveableStateHolder,
content: @Composable () -> Unit
) {
CompositionLocalProvider(
LocalViewModelStoreOwner provides this,
LocalLifecycleOwner provides this,
LocalSavedStateRegistryOwner provides this
) {
saveableStateHolder.SaveableStateProvider(content)
}
}
のCompositionLocalProvider
メソッドの第一引数のLocalViewModelStoreOwner provides this
が「第1フィールドとしてLocalViewModelStoreOwner
、第2フィールドとして現在表示しているNavBackStackEntry
が入っているdataクラスのようなもの」だということが分かりました。
次はCompositionLocalProvider
メソッド自体の定義を確認します。定義は以下です。
/**
* [CompositionLocalProvider] binds values to [ProvidableCompositionLocal] keys. Reading the
* [CompositionLocal] using [CompositionLocal.current] will return the value provided in
* [CompositionLocalProvider]'s [values] parameter for all composable functions called directly
* or indirectly in the [content] lambda.
*
* @sample androidx.compose.runtime.samples.compositionLocalProvider
*
* @see CompositionLocal
* @see compositionLocalOf
* @see staticCompositionLocalOf
*/
@Composable
@OptIn(InternalComposeApi::class)
fun CompositionLocalProvider(vararg values: ProvidedValue<*>, content: @Composable () -> Unit) {
currentComposer.startProviders(values)
content()
currentComposer.endProviders()
}
メソッドのjavadocにあるように、
CompositionLocalProvider binds values to ProvidableCompositionLocal keys.
Reading the CompositionLocal using CompositionLocal.current will return the value
provided in CompositionLocalProvider's values parameter for all composable functions called directly or indirectly
in the content lambda.
こちらのメソッドを使用すると(引数のProvidedValue
の配列values
の1要素の第1フィールド)CompositionLocal
のCompositionLocal.current
を呼んだ時の値を(第2フィールドの)value
に変更。した状態で引数content‘としてわたってきた
@Composable () -> Unit`メソッドを呼んでくれます。
つまり、今見ているシチュエーションでは、「第1フィールドとしてLocalViewModelStoreOwner
、第2フィールドとして現在表示しているNavBackStackEntry
が入っている」ものが渡ってきている状況を見ていたので、このメソッドによりこのCompositionLocalProvider
の引数content
はLocalViewModelStoreOwner.current
がNavBackStackEntry
になる状態で実行されることが分かります。(…②)
今見ているシチュエーションでは、content
は前述の呼び出し側で渡している以下のラムダ式です。
{
saveableStateHolder.SaveableStateProvider(content)
}
更に、saveableStateHolder.SaveableStateProvider(content)
のcontent
はこれまた前述のNavBackStackEntry.LocalOwnersProvider
メソッドの第2引数なのでこの場合以下のラムダ式です。
{
(lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
}
このラムダ式に出てくる、ComposeNavigator.Destination.content
の定義は、
/**
* NavDestination specific to [ComposeNavigator]
*/
@NavDestination.ClassType(Composable::class)
public class Destination(
navigator: ComposeNavigator,
internal val content: @Composable (NavBackStackEntry) -> Unit
) : NavDestination(navigator)
となっています。(…③)
さて、ここで、最初にアプリケーション側からNavHost
メソッドを呼ぶ時にナビゲーショングラフを作成するときに使用するNavGraphBuilder.composable
メソッドの定義を見てみます。
public fun NavGraphBuilder.composable(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable (NavBackStackEntry) -> Unit
) {
addDestination(
ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
this.route = route
arguments.forEach { (argumentName, argument) ->
addArgument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
addDeepLink(deepLink)
}
}
)
}
メソッド内の処理の2行目ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
より、ComposeNavigator.Destination.content
にはNavGraphBuilder.composable
メソッドを呼ぶときに渡すラムダ式部分が来ることが分かりました。(…④)
③、④より
{
saveableStateHolder.SaveableStateProvider(content)
}
でsaveableStateHolder.SaveableStateProvider
に渡しているcontent
は「アプリケーション側でNavHost
メソッドを呼ぶときに渡すラムダ式の中でNavGraphBuilder.composable
メソッドを呼んでいるところで渡すラムダ式部分を、現在表示中のNavBackStackEntry
を引数として使用して呼び出す」内容なことが分かりました。
ここでsaveableStateHolder.SaveableStateProvider
の方の処理を確認すると、以下のようになっています。
@Composable
private fun SaveableStateHolder.SaveableStateProvider(content: @Composable () -> Unit) {
val viewModel = viewModel<BackStackEntryIdViewModel>()
viewModel.saveableStateHolder = this
SaveableStateProvider(viewModel.id, content)
DisposableEffect(viewModel) {
onDispose {
viewModel.saveableStateHolder = null
}
}
}
ここで見るべきなのは真ん中あたりにあるSaveableStateProvider(viewModel.id, content)
の処理です。このメソッドの定義はinterface SaveableStateHolder
にあり、実装はSaveableStateHolderImpl
クラスにあって以下のようになっています。
@Composable
override fun SaveableStateProvider(key: Any, content: @Composable () -> Unit) {
ReusableContent(key) {
val registryHolder = remember {
require(parentSaveableStateRegistry?.canBeSaved(key) ?: true) {
"Type of the key $key is not supported. On Android you can only use types " +
"which can be stored inside the Bundle."
}
RegistryHolder(key)
}
CompositionLocalProvider(
LocalSaveableStateRegistry provides registryHolder.registry,
content = content
)
// (以下略)
}
}
上の、
CompositionLocalProvider(
LocalSaveableStateRegistry provides registryHolder.registry,
content = content
)
で、saveableStateHolder.SaveableStateProvider
に渡しているcontent
==「アプリケーション側でNavHost
メソッドを呼ぶときに渡すラムダ式の中でNavGraphBuilder.composable
メソッドを呼んでいるところで渡すラムダ式部分を、現在表示中のNavBackStackEntry
を引数として使用して呼び出す」を第2引数(=content
)として、LocalSaveableStateRegistry.current
を上書きするCompositionLocalProvider
を呼んでいます。
さて、ここで、「LocalSaveableStateRegistry.current
を上書きする」といっていますが、そもそもSaveableStateProvider(viewModel.id, content)
自体、LocalViewModelStoreOwner.current
やLocalViewModelStoreOwner.current
,LocalSavedStateRegistryOwner.current
を上書きするCompositionLocalProvider
のcontent
として呼び出されるものでした。
ゆえにSaveableStateProvider(viewModel.id, content)
のCompositionLocalProvider
で「LocalSaveableStateRegistry.current
を上書き」される前の他のcurrent
は上書きされたもの、そしてそれは②よりLocalViewModelStoreOwner.current
はNavBackStackEntry
です。
つまり、「アプリケーション側でNavHost
メソッドを呼ぶときに渡すラムダ式の中でNavGraphBuilder.composable
メソッドを呼んでいるところで渡すラムダ式」部分は、LocalViewModelStoreOwner.current
がNavBackStackEntry
の状態で呼びだされます。(…⑤)
ということにより
コンポーズ可能なメソッド内でviewModel()を呼んだ時何が起きるのかの結論①と、ナビゲーショングラフ内で何をしているかの結論⑤より、
コンポーズ可能なメソッド内でviewModel()
を呼んだ時はVideModelStoreOwner
としてLocalViewModelStoreOwner.current
が使われ、LocalViewModelStoreOwner.current
はナビゲーショングラフ内では、今表示中のNavBackStackEntry
(≒今表示中のパスの画面)になることが分かります。
つまり、ナビゲーショングラフはデスティネーション毎(パス毎)に独立したViewModelStoreOwner
を持ち、viewModel()
を呼んだ時はパス毎に異なるViewModel
のインスタンスが取得出来て、それらはナビゲーショングラフの載っているActivity
やFragment
のものとは別のインスタンスです。
注意
※ 2022/10/9 追記 : 公開当初下の内容を書いてしまっていましたが、間違っています。
ここまでの議論の通りNavGraphBuilder.composable
メソッドのラムダ式の中はNavBackStackEntry
に紐づいたViewModel
が取得できます。
「注意」セクションを本編部分の執筆を書く前に書いてあったのですが、この記事を書き始めた時点ではそもそもサンプルコードが間違っていて、それにこじつけるような内容になっていました。本編執筆の過程で理解も深まり、以前とは大筋の結論が変わったりしたのですが「注意」の章に関して最後に見直すこともほぼせず最初に書いたものをそのまま公開してしまっていました。。。
現在の理解としては、この記事の話題について、特段注意すべきことはないと考えています。
一応、以前のバージョンのサンプルコードのスニペットとともに残しておきます。
以下の、
NavHost(navController = navController, startDestination = "home") {
composable("home") {
Log.i(TAG, "NavStackEntry Home1 viewModel id : " + viewModel.getId())
Home1()
}
composable("home2") {
Log.i(TAG, "NavStackEntry Home2 viewModel id : " + viewModel.getId())
Home2()
}
}
Log.i(TAG, "NavStackEntry Home1 viewModel id : " + viewModel.getId())
やLog.i(TAG, "NavStackEntry Home2 viewModel id : " + viewModel.getId())
をしている場所は、引数にNavBackStackEntry
が渡ってきているラムダメソッドの場所だが、ここではまだLocalViewModelStoreOwner.current
はNavBackStackEntry
でない。その中で画面を作成するために呼ばれたHome1()
やHome2()
以降がLocalViewModelStoreOwner.current
=NavBackStackEntry
。
感想
- 書き終わった勢いで公開せず、ちゃんと補足の章まで含めてすべてきちんと見直すべきですね。。当たり前ですが。。。