首次启动App从Drawer导航至指定页面触发NullPointerException崩溃
问题描述
MainScreen页面通过Drawer实现导航,但App首次启动时,导航至两个特定页面会触发崩溃,抛出NullPointerException,报错位置为androidx.compose.ui.platform.RenderNodeLayer.updateDisplayList。仅通过Drawer导航至这些页面,未进行其他操作。
崩溃栈信息
FATAL EXCEPTION: main Process: com.tags.taglife, PID: 21478 java.lang.NullPointerException at androidx.compose.ui.platform.RenderNodeLayer.updateDisplayList(RenderNodeLayer.android.kt:245) at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:760) at android.view.View.draw(View.java:24398) at android.view.View.updateDisplayListIfDirty(View.java:23256) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4732) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4704) at android.view.View.updateDisplayListIfDirty(View.java:23203) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4732) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4704) at android.view.View.updateDisplayListIfDirty(View.java:23203) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4732) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4704) at android.view.View.updateDisplayListIfDirty(View.java:23203) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4732) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4704) at android.view.View.updateDisplayListIfDirty(View.java:23203) at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:753) at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:759) at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:857) at android.view.ViewRootImpl.draw(ViewRootImpl.java:5501) at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:5194) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:4356) at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2991) at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:10665) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1301) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1309) at android.view.Choreographer.doCallbacks(Choreographer.java:923) at android.view.Choreographer.doFrame(Choreographer.java:852) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1283) at android.os.Handler.handleCallback(Handler.java:942) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loopOnce(Looper.java:226) at android.os.Looper.loop(Looper.java:313) at android.app.ActivityThread.main(ActivityThread.java:8741) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1067)
其中一个崩溃页面的代码
package com.tags.taglife.presentation.MainScreen.sidebar import android.annotation.SuppressLint import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.verticalScroll import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import com.tags.taglife.presentation.MainScreen.MainViewModel import com.tags.taglife.presentation.MainScreen.NavScreens import com.tags.taglife.presentation.ProgressScreen import com.tags.taglife.presentation.tutorial.ItemModel import com.tags.taglife.presentation.tutorial.TutorialViewModel import com.tags.taglife.ui.theme.bottomLine import com.tags.taglife.ui.theme.white import taglife.R @SuppressLint("CoroutineCreationDuringComposition") @Composable fun Tutorial( modifier: Modifier = Modifier, viewModel: MainViewModel, tutorialViewModel: TutorialViewModel = hiltViewModel() ) { viewModel.setCurrentScreen(NavScreens.DrawerScreens.Tutorial) val isDark = tutorialViewModel.isDarkTheme val loading = tutorialViewModel.isLoaded if (!loading.value) ProgressScreen() else { Column( modifier = modifier .fillMaxWidth() .verticalScroll(rememberScrollState()) .padding(20.dp), ) { //INSTRUCTIONS FOR USE Title(R.string.instructions_for_use) tutorialViewModel.instructions.mapIndexed { index, item -> Item(isDark = isDark.value, item = item, index = index + 1) } Spacer(modifier = Modifier.height(15.dp)) //FUNCTIONS Title(text = R.string.functions_of_homepage_buttons) tutorialViewModel.functions.mapIndexed { index, item -> Item(isDark = isDark.value, item = item, index = index + 1) } Spacer(modifier = Modifier.height(15.dp)) //QR CODE Title(text = R.string.qr_code_functions) tutorialViewModel.qrCodeFunctions.mapIndexed { index, item -> Item(isDark = isDark.value, item = item, index = index + 1) } Spacer(modifier = Modifier.height(15.dp)) //NON NFC SCAN TAG Title(text = R.string.non_nfc_scan_tag) tutorialViewModel.nonNFCScanTag.mapIndexed { index, item -> Item(isDark = isDark.value, item = item, index = index + 1) } } } } @Composable fun Title(text: Int) { Text( text = stringResource(id = text), fontWeight = FontWeight.Bold, fontSize = 20.sp, color = MaterialTheme.colors.primary, ) } @Composable fun Item( isDark : Boolean, item: List<ItemModel>, index: Int ) { val myId = index.toString() var image: Int = R.drawable.vector var mainText = buildAnnotatedString { append("$index. ") item.forEach { if (it.text != null) append(stringResource(id = it.text!!)) if (it.icon != null) { image = it.icon!! appendInlineContent(myId, "[icon]") } } } val inlineContent = mapOf( Pair( myId, InlineTextContent( Placeholder( width = 30.sp, height = 30.sp, placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter ) ) { Image( painter = painterResource(image), modifier = Modifier.padding(5.dp), contentDescription = "", colorFilter = if (isDark) ColorFilter.tint(white) else null ) } ) ) Column( modifier = Modifier .fillMaxWidth(), verticalArrangement = Arrangement.Center ) { Text( text = mainText, inlineContent = inlineContent, textAlign = TextAlign.Start, modifier = Modifier .fillMaxWidth() .padding(vertical = 12.dp) ) Spacer( modifier = Modifier .height(1.dp) .fillMaxWidth() .background(bottomLine) ) } }
解决方案
针对该RenderNodeLayer空指针崩溃,结合代码分析给出以下修复方向:
1. 修复InlineTextContent的ID冲突
Item组件中用index.toString()作为inlineContent的key,若列表存在多个带图标条目,可能出现ID重复或重组异常。建议生成唯一ID:
val myId = "${index}_${item.hashCode()}"
确保appendInlineContent的ID与inlineContent的key严格对应,避免绘制层空指针。
2. 规范ViewModel状态更新时机
当前Tutorial组件根节点直接调用viewModel.setCurrentScreen,会在每次重组时执行,易引发状态异常。将调用移至导航点击事件,或用LaunchedEffect确保仅执行一次:
LaunchedEffect(Unit) { viewModel.setCurrentScreen(NavScreens.DrawerScreens.Tutorial) }
3. 校验异步加载状态的准确性
确认tutorialViewModel.isLoaded是正确的State<Boolean>类型,且数据加载完成后正确更新。可添加日志验证首次导航时loading.value是否切换为true,避免组件树状态不一致。
4. 升级Compose依赖版本
这类RenderNodeLayer空指针问题在旧版Compose中较为常见,升级至最新稳定版(如androidx.compose.ui:ui:1.6.0及以上),底层绘制的空指针问题多已修复。
5. 确保资源ID有效性
检查ItemModel中的icon资源ID是否有效,避免无效ID导致painterResource加载失败。添加默认值处理:
image = it.icon ?: R.drawable.vector
内容的提问来源于stack exchange,提问作者Dušan Perić

