使用Kotlin DSL构建导航图时navGraphViewModels致进程崩溃
在Activity中使用Kotlin DSL初始化导航图,Fragment的onCreate()方法中通过navGraphViewModels()获取ViewModel实例时,进程死亡后重启应用会崩溃,错误信息如下:
Caused by: java.lang.IllegalArgumentException: No destination with route main_navigation is on the NavController's back stack. The current destination is null
相关代码
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val navController = (supportFragmentManager .findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController val graph = navController.createGraph( startDestination = "MyFragment1", route = "main_navigation" ) { fragment<MyFragment1>("MyFragment1") { deepLink("fi://fragment1") argument("testing") { type = NavType.StringType defaultValue = "hello" } } navigation("MyFragment2", "included_graph") { fragment<MyFragment2>("MyFragment2") { deepLink("fi://fragment2") argument("testing") { type = NavType.StringType nullable = true } } } } navController.graph = graph }
MyFragment1.kt
private val vm: Vm by navGraphViewModels("main_navigation") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) vm.someFunction() }
完整堆栈信息
Caused by: java.lang.IllegalArgumentException: No destination with route main_navigation is on the NavController's back stack. The current destination is null at androidx.navigation.NavController.getBackStackEntry(NavController.kt:2472) at com.example.testingnavigation.MyFragment1$special$$inlined$navGraphViewModels$default$1.invoke(NavGraphViewModelLazy.kt:209) at com.example.testingnavigation.MyFragment1$special$$inlined$navGraphViewModels$default$1.invoke(NavGraphViewModelLazy.kt:208) at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74) at androidx.navigation.NavGraphViewModelLazyKt.navGraphViewModels$lambda-3(NavGraphViewModelLazy.kt:208) at androidx.navigation.NavGraphViewModelLazyKt.access$navGraphViewModels$lambda-3(NavGraphViewModelLazy.kt:1) at com.example.testingnavigation.MyFragment1$special$$inlined$navGraphViewModels$default$4.invoke(NavGraphViewModelLazy.kt:217) at com.example.testingnavigation.MyFragment1$special$$inlined$navGraphViewModels$default$4.invoke(NavGraphViewModelLazy.kt:214) at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:47) at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:35) at com.example.testingnavigation.MyFragment1.getVm(MyFragment1.kt:26) at com.example.testingnavigation.MyFragment1.onCreate(MyFragment1.kt:30) at androidx.fragment.app.Fragment.performCreate(Fragment.java:3094) at androidx.fragment.app.FragmentStateManager.create(FragmentStateManager.java:504) at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:268) at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:122) at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1455) at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3034) at androidx.fragment.app.FragmentManager.dispatchCreate(FragmentManager.java:2941) at androidx.fragment.app.Fragment.restoreChildFragmentState(Fragment.java:1989) at androidx.fragment.app.Fragment.onCreate(Fragment.java:1967) at androidx.navigation.fragment.NavHostFragment.onCreate(NavHostFragment.kt:163) at androidx.fragment.app.Fragment.performCreate(Fragment.java:3094) at androidx.fragment.app.FragmentStateManager.create(FragmentStateManager.java:504) at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:268) at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:114) at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1455) at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3034) at androidx.fragment.app.FragmentManager.dispatchCreate(FragmentManager.java:2941) at androidx.fragment.app.FragmentController.dispatchCreate(FragmentController.java:252) at androidx.fragment.app.FragmentActivity.onCreate(FragmentActivity.java:219) at com.example.testingnavigation.MainActivity.onCreate(MainActivity.kt:17) at android.app.Activity.performCreate(Activity.java:8119) at android.app.Activity.performCreate(Activity.java:8103) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1359) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3713) ... 11 more
崩溃复现步骤
- 打开应用并将其置于后台
- 执行命令:
adb shell am kill <package_name> - 从应用图标重新启动应用
直接在activity_main.xml布局文件中设置导航图则不会崩溃,请问使用Kotlin DSL实现导航的正确方式是什么?
问题核心是进程恢复时的时序冲突:系统恢复Activity和Fragment时,Fragment的重建会先于Activity完成导航图的初始化,此时Fragment调用navGraphViewModels("main_navigation")时,NavController的回退栈中还没有对应导航图的条目,因此触发报错。
方法一:自定义NavHostFragment提前初始化导航图
不要在Activity的onCreate()中动态设置导航图,而是通过自定义NavHostFragment,在其自身的onCreate()方法中用DSL创建导航图,确保导航图在Fragment重建前完成初始化:
class DslNavHostFragment : NavHostFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val navController = navController val graph = navController.createGraph( startDestination = "MyFragment1", route = "main_navigation" ) { fragment<MyFragment1>("MyFragment1") { deepLink("fi://fragment1") argument("testing") { type = NavType.StringType defaultValue = "hello" } } navigation("MyFragment2", "included_graph") { fragment<MyFragment2>("MyFragment2") { deepLink("fi://fragment2") argument("testing") { type = NavType.StringType nullable = true } } } } navController.graph = graph } }
随后在activity_main.xml中使用这个自定义NavHostFragment:
<androidx.fragment.app.FragmentContainerView android:id="@+id/nav_host_fragment" android:name="com.example.testingnavigation.DslNavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" />
方法二:延迟ViewModel的获取时机
如果不想自定义NavHostFragment,可以将ViewModel的获取延迟到Fragment的onViewCreated()生命周期方法中,此时Activity已经完成导航图的设置,NavController状态也已恢复:
private lateinit var vm: Vm override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) vm = navGraphViewModels("main_navigation").value vm.someFunction() }
注意:这种方法需要处理ViewModel初始化延迟的空安全问题,不如第一种方案优雅。
补充说明
直接在布局中设置导航图不会崩溃的原因是:系统恢复NavHostFragment时,会直接从布局资源中读取导航图并完成初始化,Fragment重建时导航图已经存在于NavController中。使用DSL动态设置时,必须保证导航图的初始化时机早于Fragment的onCreate()。
内容的提问来源于stack exchange,提问作者Saran Sankaran

