Compose LazyColumn列表项点击触发应用崩溃问题排查
问题:Jetpack Compose LazyColumn点击列表项崩溃(IndexOutOfBoundsException)
使用LazyColumn展示列表时,点击列表项会触发IndexOutOfBoundsException导致应用崩溃;将LazyColumn替换为普通Column后无崩溃问题。
相关代码
ViewModel代码
package com.example.workspace.viewmodels import androidx.lifecycle.ViewModel import com.example.workspace.ui.screens.FunnelListItem import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel class FunnelScreenViewModel @Inject constructor( ) : ViewModel() { val ITEM_AREA = 0 val ITEM_HQ = 1 val ITEM_PATCH = 2 val ITEM_CUSTOMER = 3 val screenTitles = listOf( "Select Area", "Select HQ", "Select Patch", "Select Customer" ) val startingPoint = ITEM_AREA fun getAreas() = mutableListOf( FunnelListItem(ITEM_AREA, 1, "Coimbatore BM"), FunnelListItem(ITEM_AREA, 2, "Madurai BM"), FunnelListItem(ITEM_AREA, 3, "Trichy BM"), ) fun getHQs() = mutableListOf( FunnelListItem(ITEM_HQ, 1, "Madurai 01"), FunnelListItem(ITEM_HQ, 2, "Madurai 02"), ) fun getPatches() = mutableListOf( FunnelListItem(ITEM_PATCH, 1, "Madurai"), FunnelListItem(ITEM_PATCH, 2, "Theni"), ) fun getCustomers() = mutableListOf( FunnelListItem(ITEM_CUSTOMER, 1, "Saravanan"), FunnelListItem(ITEM_CUSTOMER, 2, "Pandian"), FunnelListItem(ITEM_CUSTOMER, 3, "Anbu"), FunnelListItem(ITEM_CUSTOMER, 4, "Anbu"), FunnelListItem(ITEM_CUSTOMER, 5, "Anbu") ) }
Screen可组合项代码
package com.example.workspace.ui.screens import androidx.activity.compose.BackHandler import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import com.example.workspace.ui.components.SimpleTopAppBar import com.example.workspace.ui.navigation.Screen import com.example.workspace.viewmodels.FunnelScreenViewModel @Composable fun FunnelScreen( navHostController: NavHostController, funnelScreenViewModel: FunnelScreenViewModel = hiltViewModel() ){ val startingPoint = funnelScreenViewModel.startingPoint var currentItemType by rememberSaveable { mutableIntStateOf(funnelScreenViewModel.startingPoint) } val listItems = when(currentItemType) { funnelScreenViewModel.ITEM_AREA -> funnelScreenViewModel.getAreas() funnelScreenViewModel.ITEM_HQ -> funnelScreenViewModel.getHQs() funnelScreenViewModel.ITEM_PATCH -> funnelScreenViewModel.getPatches() else -> funnelScreenViewModel.getCustomers() } fun handleBack() { if (currentItemType == 0) { navHostController.popBackStack() } else { currentItemType-- } } BackHandler { handleBack() } Scaffold( topBar = { SimpleTopAppBar(title = funnelScreenViewModel.screenTitles[currentItemType]) { handleBack() } } ) { paddingValues -> FunnelScreenContent(Modifier.padding(paddingValues), listItems, onItemClicked = { if (currentItemType < 3) { currentItemType++ } else { navHostController.navigate(Screen.CallDetailsScreen.route) } }) } } @Composable fun FunnelScreenContent( modifier: Modifier, listItems: List<FunnelListItem>, onItemClicked: () -> Unit ) { LazyColumn(modifier.fillMaxWidth()){ items(listItems) { listItem -> Text( text = listItem.name, modifier = Modifier .fillMaxWidth() .height(50.dp) .clickable { onItemClicked() } ) } } } data class FunnelListItem( val itemType: Int, val id: Int, val name: String )
崩溃堆栈信息
FATAL EXCEPTION: main Process: com.seluriapharma.workspace, PID: 31984 java.lang.IndexOutOfBoundsException: Index 2, size 2 at androidx.compose.foundation.lazy.layout.MutableIntervalList.checkIndexBounds(IntervalList.kt:183) at androidx.compose.foundation.lazy.layout.MutableIntervalList.get(IntervalList.kt:166) at androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent.getKey(LazyLayoutIntervalContent.kt:86) at androidx.compose.foundation.lazy.LazyListItemProviderImpl.getKey(LazyListItemProvider.kt:85) at androidx.compose.foundation.lazy.LazyListMeasuredItemProvider.getAndMeasure(LazyListMeasuredItemProvider.kt:46) at androidx.compose.foundation.lazy.LazyListMeasureKt.createItemsAfterList(LazyListMeasure.kt:418) at androidx.compose.foundation.lazy.LazyListMeasureKt.measureLazyList-50ZNEM8(LazyListMeasure.kt:278)
替换为Column后的正常代码片段
@Composable fun FunnelScreenContent( modifier: Modifier, currentItemType: Int, funnelScreenViewModel: FunnelScreenViewModel, onItemClicked: () -> Unit ) { val listItems = when(currentItemType) { funnelScreenViewModel.ITEM_AREA -> funnelScreenViewModel.getAreas() funnelScreenViewModel.ITEM_HQ -> funnelScreenViewModel.getHQs() funnelScreenViewModel.ITEM_PATCH -> funnelScreenViewModel.getPatches() else -> funnelScreenViewModel.getCustomers() } Column(modifier.fillMaxWidth()) { listItems.forEach { item -> Text( text = item.name, modifier = Modifier .fillMaxWidth() .height(50.dp) .clickable { onItemClicked() } ) } } }
依赖配置
dependencies { implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") implementation("androidx.activity:activity-compose:1.8.0") implementation(platform("androidx.compose:compose-bom:2023.03.00")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3:1.2.0-alpha10") implementation("androidx.navigation:navigation-compose:2.7.4") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") val ktorVersion = "1.6.3" implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-android:$ktorVersion") implementation("io.ktor:ktor-client-serialization:$ktorVersion") implementation("io.ktor:ktor-client-logging:$ktorVersion") implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0") // ObjectBox dependencies implementation("io.objectbox:objectbox-android:3.7.0") // Dagger hilt implementation("com.google.dagger:hilt-android:2.48.1") kapt("com.google.dagger:hilt-android-compiler:2.44") implementation("androidx.hilt:hilt-navigation-compose:1.0.0") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00")) androidTestImplementation("androidx.compose.ui:ui-test-junit4") debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") }
原因分析
- LazyColumn的
items方法默认使用元素的位置索引作为复用key,当点击列表项更新currentItemType后,listItems被替换为新列表(比如从3个元素的Area列表切换到2个元素的HQ列表),LazyColumn会尝试复用之前已渲染的item组件,但新列表的元素数量少于旧列表,导致访问超出新列表长度的索引,触发IndexOutOfBoundsException。 - 普通Column没有组件复用逻辑,每次状态更新都会重新渲染所有元素,因此不会出现索引越界问题。
- 另外,ViewModel中每次调用
getAreas()等方法都会返回新的MutableList实例,且未提供稳定唯一的标识key,进一步加剧了LazyColumn的复用冲突。
修复方案
1. 为LazyColumn的items指定稳定唯一的key
修改FunnelScreenContent中的LazyColumn代码,使用FunnelListItem的id作为唯一key,让LazyColumn能正确识别元素身份,避免错误复用:
@Composable fun FunnelScreenContent( modifier: Modifier, listItems: List<FunnelListItem>, onItemClicked: () -> Unit ) { LazyColumn(modifier.fillMaxWidth()){ // 指定key为item的id,确保元素身份唯一 items(listItems, key = { it.id }) { listItem -> Text( text = listItem.name, modifier = Modifier .fillMaxWidth() .height(50.dp) .clickable { onItemClicked() } ) } } }
2. 优化ViewModel的列表提供方式(可选但推荐)
将ViewModel中的可变列表改为不可变列表并缓存,避免每次调用都创建新实例,提升性能同时增强稳定性:
@HiltViewModel class FunnelScreenViewModel @Inject constructor() : ViewModel() { // ... 其他代码保持不变 // 改为缓存的不可变列表 private val areas = listOf( FunnelListItem(ITEM_AREA, 1, "Coimbatore BM"), FunnelListItem(ITEM_AREA, 2, "Madurai BM"), FunnelListItem(ITEM_AREA, 3, "Trichy BM"), ) fun getAreas() = areas private val hQs = listOf( FunnelListItem(ITEM_HQ, 1, "Madurai 01"), FunnelListItem(ITEM_HQ, 2, "Madurai 02"), ) fun getHQs() = hQs private val patches = listOf( FunnelListItem(ITEM_PATCH, 1, "Madurai"), FunnelListItem(ITEM_PATCH, 2, "Theni"), ) fun getPatches() = patches private val customers = listOf( FunnelListItem(ITEM_CUSTOMER, 1, "Saravanan"), FunnelListItem(ITEM_CUSTOMER, 2, "Pandian"), FunnelListItem(ITEM_CUSTOMER, 3, "Anbu"), FunnelListItem(ITEM_CUSTOMER, 4, "Anbu"), FunnelListItem(ITEM_CUSTOMER, 5, "Anbu") ) fun getCustomers() = customers }
内容的提问来源于stack exchange,提问作者Indra Kumar S
相关产品推荐
相关产品推荐

