You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

首次启动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ć

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 12:55:35