Jetpack Compose跳转新页面时应用崩溃(空指针异常)
我正在开发一款倒计时应用,启动页面用于填写信息,计时功能及展示在另一页面实现。但从启动页跳转至计时页时应用崩溃,Logcat抛出空指针异常,栈信息如下:
java.lang.NullPointerException
at androidx.navigation.NavController.navigate(NavController.kt:1652)
at androidx.navigation.NavController.navigate(NavController.kt:1984)
at androidx.navigation.NavController.navigate$default(NavController.kt:1979)
导航宿主与NavController配置
package com.example.myapplication.ui import androidx.compose.runtime.Composable import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController enum class TimerRoutes() { StartScreen, TimerScreen } @Composable fun TimerApp() { val navController: NavHostController = rememberNavController() NavHost(navController = navController, startDestination = TimerRoutes.StartScreen.name) { composable(route = TimerRoutes.StartScreen.name) { TimerStartScreen(navController) } composable(route = TimerRoutes.TimerScreen.name) { TimerScreen() } } }
TimerStartScreen代码
package com.example.myapplication.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @OptIn(ExperimentalMaterial3Api::class) @Composable fun TimerStartScreen(navController: NavHostController) { val viewModel: TimerViewModel = viewModel() val timerUiState = viewModel.uiState.collectAsState() Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) { Column() { TopAppBar(title = {Text(text = "Start Screen", textAlign = TextAlign.Center)}) TextField( value = timerUiState.value.name, onValueChange = { viewModel.updateNameText(it) }, Modifier .fillMaxWidth() .padding(top = 16.dp, start = 16.dp, end = 16.dp), label = { Text(text = "Enter Timer Name") } ) Spacer(modifier = Modifier.height(8.dp)) TextField( value = timerUiState.value.date, onValueChange = {viewModel.updateCurrentDateNumber(it)}, Modifier .fillMaxWidth() .padding(start = 16.dp, end = 16.dp), label = { Text(text = "Enter Date of Event (dd/MM/yyyy)")}, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), ) Spacer(modifier = Modifier.height(30.dp)) Button( onClick = {navController.navigate(TimerRoutes.TimerScreen.name)}, content = { Text(text = "Create!")}, modifier = Modifier.padding(start = 135.dp) ) } } }
TimerScreen代码
package com.example.myapplication.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun TimerScreen() { val viewModel: TimerViewModel = viewModel() val timerUiState = viewModel.uiState.collectAsState() viewModel.findNumberOfDaysAndHoursLeft(timerUiState.value.date) Box(contentAlignment = Alignment.TopCenter) { Column() { Spacer(modifier = Modifier.height(30.dp)) Row(){ Text(text = "Days: ${timerUiState.value.diffInDays.toString()}", modifier = Modifier.padding(start = 110.dp)) Spacer(modifier = Modifier.width(15.dp)) Text(text = "Hours: ${timerUiState.value.diffInHours.toString()}") } } } }
问题排查与修复
1. 核心崩溃原因:错误的NavController实例
你的TimerStartScreen中导入了rememberNavController、NavHost等导航API,但这个Composable不需要这些——你已经通过参数接收了上层TimerApp传递的navController。如果在这个Composable内部不小心调用了rememberNavController(),会生成一个未关联到NavHost的独立实例,调用navigate时就会触发空指针。
修复: 删除TimerStartScreen中多余的导航导入:
// 移除这三行无用导入 import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController
2. 额外问题:ViewModel实例不共享
当前TimerStartScreen和TimerScreen各自通过viewModel()获取实例,这会导致两个页面拿到不同的ViewModel对象——启动页填写的数据无法传递到计时页,还可能引发后续空值问题。
修复: 在TimerApp中创建共享ViewModel并传递给子页面:
@Composable fun TimerApp() { val navController: NavHostController = rememberNavController() val viewModel: TimerViewModel = viewModel() // 全局共享的ViewModel实例 NavHost(navController = navController, startDestination = TimerRoutes.StartScreen.name) { composable(route = TimerRoutes.StartScreen.name) { TimerStartScreen(navController, viewModel) // 传递ViewModel } composable(route = TimerRoutes.TimerScreen.name) { TimerScreen(viewModel) // 传递ViewModel } } }
修改TimerStartScreen参数:
@OptIn(ExperimentalMaterial3Api::class) @Composable fun TimerStartScreen(navController: NavHostController, viewModel: TimerViewModel) { val timerUiState = viewModel.uiState.collectAsState() // 其余代码不变 }
修改TimerScreen参数:
@Composable fun TimerScreen(viewModel: TimerViewModel) { val timerUiState = viewModel.uiState.collectAsState() // 其余代码不变 }
3. 防御性建议:处理空日期
在ViewModel的findNumberOfDaysAndHoursLeft方法中,先判断日期字符串是否为空或格式错误,避免后续逻辑触发空指针:
fun findNumberOfDaysAndHoursLeft(dateStr: String?) { if (dateStr.isNullOrEmpty()) { // 处理空日期情况,比如更新uiState提示用户 return } // 原日期处理逻辑 }
内容的提问来源于stack exchange,提问作者Allen J Mathew

