たくさんの自由帳

Navigation3 にしたら AndroidViewModel が使えなかったので付け焼き刃する

投稿日 : | 0 日前

文字数(だいたい) : 1588

あけましておめでとうございます!!!
年明けはM.2 SSDを換装してWindows 11にアップデートしてました。起動音がなるんだ。

さきにこたえ

CreationExtrasApplicationがないエラーだったので、付け焼き刃ですがApplicationを渡せば解決。
知らないうちにLocalActivityなるものが生えてる

SubScreen(
    viewModel = viewModel(
        extras = MutableCreationExtras(
            (LocalViewModelStoreOwner.current as? HasDefaultViewModelProviderFactory)?.defaultViewModelCreationExtras ?: CreationExtras.Empty
        ).apply {
            // Application を渡す付け焼き刃
            set(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY, LocalActivity.current!!.application)
        }
    ),
    title = "ホーム画面"
)

本題

Navigation3と遊んでました。

感想はまたあとで!そんなことより!

java.lang.IllegalArgumentException: CreationExtras must have an application by `APPLICATION_KEY`

Navigation3でも同じようにViewModelをサポートしていますが、ついに、AndroidViewModelが切られてしまった。
もともとViewModelContextを直で使うべきではないってAndroidが公式で言ってたので・・・

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            AndroidNavigation3AndroidViewModelTheme {
                MainScreen()
            }
        }
    }
}

class HomeViewModel(application: Application) : AndroidViewModel(application) // Android の ViewModel がエラーに...

@Composable
private fun MainScreen() {
    val backStack = rememberNavBackStack(Links.Home)
    NavDisplay(
        backStack = backStack,
        entryDecorators = listOf(
            rememberSaveableStateHolderNavEntryDecorator(),
            rememberViewModelStoreNavEntryDecorator()
        ),
        entryProvider = entryProvider {
            entry<Links.Home> { link ->
                SubScreen(
                    viewModel = viewModel(),
                    title = "ホーム画面"
                )
            }
        }
    )
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SubScreen(
    viewModel: HomeViewModel,
    title: String
) {
    Scaffold(
        topBar = { TopAppBar(title = { Text(title) }) }
    ) { innerPadding ->
        // do nothing
    }
}

sealed interface Links : NavKey {
    @Serializable
    data object Home : Links
}

環境

Android Studio Otter 2 Feature Drop | 2025.2.2 Patch 1

[versions]
# 省略...
nav3Core = "1.0.0"
lifecycleViewmodelNav3 = "2.10.0"

[libraries]
# 省略...
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }

[plugins]
# 省略...
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

直し方

冒頭の通りです。Issueを見る限りApplicationがなくなったのは期待通りらしい。

あと、追加で引数を渡したい場合はfactory =の引数を埋めればよいです。

class HomeViewModel(application: Application, links: Links) : AndroidViewModel(application) // 画面遷移の data class も欲しい
SubScreen(
    viewModel = viewModel(
        extras = MutableCreationExtras(
            (LocalViewModelStoreOwner.current as? HasDefaultViewModelProviderFactory)?.defaultViewModelCreationExtras ?: CreationExtras.Empty
        ).apply {
            // Application を渡す付け焼き刃
            set(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY, LocalActivity.current!!.application)
        },
        factory = viewModelFactory {
            initializer {
                HomeViewModel(
                    application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]!!,
                    links = it // 引数がある場合
                )
            }
        }
    ),
    title = "ホーム画面"
)

おわり。

ソースコード

いる?

Navigation3 感想

まだあんまり使えてないけど。真髄はSceneとかなんだと思う。

Navigation2であったCompose Navigationとの違いは、バックスタック(画面遷移履歴)をつかさどるrememberNavBackStack()が、MutableListを実装している点でしょうか。
画面遷移はMutableListを自分で操作することによって行うようになりました。add()すれば画面が切り替わり、removeLastOrNull()すれば画面が閉じられる。

Navigation2の時は今表示されてる画面を取得するのに、どっかのクラスにあるFlowcollectAsState()したような気がしますが、
今作ではただの配列になったので、MutableListの最後の要素を取るだけで済むはず。

あと配列を操作すればよくなったので、前の画面に戻れない挙動がすぐできるのが強い。
これが一番のメリットかもしれない。popUpTo()inclusiveをいい感じにすればいいんだろうけどあのAPIわかりにくすぎる。

entry<NavigationLinkList.PermissionScreen> {
    PermissionScreen(
        onGranted = {
            // 履歴配列から消してホームを出す
            backStack += NavigationLinkList.HomeScreen
            backStack -= NavigationLinkList.PermissionScreen
        }
    )
}
entry<NavigationLinkList.HomeScreen> {
    HomeScreen(
        onNavigate = { dest -> backStack += dest }
    )
}

きになる点としては

composable("${NavigationPaths.VideoEditor.path}/{projectName}?openVideoInfo={openVideoInfo}") {
    VideoEditorScreen(
        onNavigate = { navigationPaths -> navController.navigate(navigationPaths.path) },
        onBack = { navController.popBackStack() }
    )
}
class VideoEditorViewModel(
    private val application: Application,
    private val savedStateHandle: SavedStateHandle
) : AndroidViewModel(application) {

    // SavedStateHandle 経由で
    val projectName: String = savedStateHandle["projectName"]!!
}

画面遷移のパラメーターをSavedStateHandleに自動的に入れてくれる機能がありましたが、この機能は無くなったようです。
今のところviewModelFactory { }で引数付きViewModelを作れるようにする案と、KoinとかのDIライブラリだともっと簡単に渡せるっぽいです。DIライブラリに入門しておくんだった・・・

あとは文字列のパスからdata classとかを使ったパスになった。パラメーターが型安全の一方、kotlinx.serializationがルーティングに必要な時代かと思った。
たぶんserializationdata classも使わずenumでも動きそうな気はしますが、公式の方法から逸脱してまでする気も起きないので。