Jetpack Compose中ViewModel更新错误变量致城墙绘制异常
城图生成器城墙功能绘制问题排查
我正在为D&D/TRPG开发一款城图生成器应用,核心功能是让用户点击创建城市区域,当前重点开发城墙功能:用户点击标记城墙顶点,完成操作后生成完整城墙。技术栈采用Kotlin/Jetpack Compose的Canvas组件+ViewModel实现,流程为:用户点击顶点,坐标传入ViewModel存入列表,Canvas实时绘制该列表;当用户再次点击第一个顶点时,将列表传入CityWall对象并清空列表,随后绘制最终的城墙。
目前遇到两个核心问题:
- 进行中的城墙顶点列表无法实时绘制,仅在CityWall创建完成后才会显示;
- 创建新墙时,会逐点重新绘制已完成的旧城墙。
我已添加日志但无法理解:为何点击时更新的是CityWall,而非应该接收点击的进行中墙列表。
Activity代码
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { WallTestTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { val wallViewModel: WallViewModel = viewModel() val wallUIState by wallViewModel.uiState.collectAsState() MainScreen( wallViewModel = wallViewModel, inProgressWall = wallUIState.inProgressWall, cityWall = wallUIState.cityWall ) } } } } }//end Main Activity @Composable fun MainScreen( wallViewModel: WallViewModel, inProgressWall: MutableList<Offset>, cityWall: CityWall ){ var clickMode: ClickMode by remember { mutableStateOf(ClickMode.AWAITINGFIRSTWALLCLICK) } fun doClick(clickedPoint: Offset) { Log.d("CLICK", "at $clickedPoint") when (clickMode){ ClickMode.AWAITINGFIRSTWALLCLICK -> { Log.d("FIRST WALL", "in progress ${inProgressWall.toString()}") wallViewModel.startWall(clickedPoint) clickMode = ClickMode.AWAITINGWALLCLICK } ClickMode.AWAITINGWALLCLICK -> { if ((clickedPoint - inProgressWall[0]).magnitude() < 50) { Log.d("CLICK CLOSE", "$clickedPoint ${inProgressWall[0]}") wallViewModel.finaliseWall() clickMode = ClickMode.AWAITINGFIRSTWALLCLICK } else { Log.d("WALL CLICK", "in progress ${inProgressWall.toString()}") wallViewModel.continueWall(clickedPoint) clickMode = ClickMode.AWAITINGWALLCLICK } } else -> {} } } Canvas( modifier = Modifier .fillMaxSize() .padding(10.dp) .background(color = Color.LightGray) .pointerInput(Unit) { detectTapGestures( onTap = { doClick(it) } ) } ){ fun drawInProgressWall(wall: MutableList<Offset>) { for (towerIndex in wall.indices){ drawCircle(color = Color.Red, radius = 30f, center = wall[towerIndex]) drawLine( Color.Cyan, start = wall[towerIndex], end = wall[(towerIndex + 1) % wall.size], strokeWidth = 5f ) } } fun drawWall(wall: CityWall) { Log.d("DRAW WALL", wall.vertexes.toString()) for (towerIndex in wall.vertexes.indices){ drawCircle(color = Color.Black, radius = 30f, center = wall.vertexes[towerIndex]) drawLine( Color.Black, start = wall.vertexes[towerIndex], end = wall.vertexes[(towerIndex + 1) % wall.vertexes.size], strokeWidth = 20f ) } } drawWall(cityWall) drawInProgressWall(inProgressWall) }//end canvas Box(modifier = Modifier.fillMaxSize()){ Row( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter), horizontalArrangement = Arrangement.SpaceEvenly ){ Text(text = clickMode.toString()) } } }//end Main Screen fun Offset.magnitude(): Float { return sqrt((this.x * this.x) + (this.y * this.y)) }
ViewModel代码
class WallViewModel : ViewModel() { var inProgressWall: MutableList<Offset> = mutableListOf() var cityWall = CityWall(vertexes = mutableListOf()) private val _uiState = MutableStateFlow( WallUiState( inProgressWall = inProgressWall, cityWall = cityWall ) ) val uiState: StateFlow<WallUiState> = _uiState.asStateFlow() fun updateState() { Log.d("UPDATE", "InProgress is $inProgressWall") Log.d("UPDATE", "wall is ${cityWall.vertexes}") _uiState.update { it -> it.copy( inProgressWall = inProgressWall, cityWall = cityWall ) } } fun startWall(clicked: Offset) { Log.d("START WALL", "at $clicked") inProgressWall.clear() inProgressWall.add(clicked) updateState() }//end Start Wall fun continueWall(clicked: Offset) { Log.d("CONTINUE WALL", "at $clicked") inProgressWall.add(clicked) updateState() } fun finaliseWall() { Log.d("FINISH WALL", "END") cityWall.vertexes = inProgressWall Log.d("NEW WALL BEFORE CLEAR", cityWall.vertexes.toString()) inProgressWall.clear() Log.d("NEW WALL", cityWall.vertexes.toString()) updateState() return } }//end view model data class WallUiState( val inProgressWall: MutableList<Offset>, val cityWall: CityWall )
问题根源分析
- MutableList引用未更新导致Compose不重组:ViewModel中的
inProgressWall是MutableList,调用clear()/add()时仅修改列表内容,列表引用未变。StateFlow的update方法传入的是同一个列表引用,Compose的collectAsState()无法感知内容变化,因此Canvas不会触发重绘,导致进行中的墙无法实时显示。 - 列表引用共享导致数据混乱:
finaliseWall()中直接将cityWall.vertexes = inProgressWall,让两者指向同一个MutableList实例。后续inProgressWall.clear()会同时清空cityWall.vertexes,而创建新墙时往inProgressWall添加数据,cityWall.vertexes也会同步变化,这就是旧城墙被逐点绘制的原因。
解决方案
- 改用不可变列表,每次更新创建新实例:将
inProgressWall改为List<Offset>,每次更新时生成新列表,确保StateFlow能感知到变化触发重组。 - 复制列表内容而非传递引用:
finaliseWall()中复制inProgressWall的内容到cityWall,避免两者共享引用。 - 将CityWall的vertexes设为不可变:防止后续意外修改已完成的城墙数据。
修改后的ViewModel示例:
class WallViewModel : ViewModel() { private var inProgressWall: List<Offset> = emptyList() private var cityWall = CityWall(vertexes = emptyList()) private val _uiState = MutableStateFlow( WallUiState( inProgressWall = inProgressWall, cityWall = cityWall ) ) val uiState: StateFlow<WallUiState> = _uiState.asStateFlow() private fun updateState() { _uiState.update { it.copy( inProgressWall = inProgressWall, cityWall = cityWall ) } } fun startWall(clicked: Offset) { inProgressWall = listOf(clicked) updateState() } fun continueWall(clicked: Offset) { inProgressWall = inProgressWall + clicked updateState() } fun finaliseWall() { cityWall = cityWall.copy(vertexes = inProgressWall.toMutableList()) inProgressWall = emptyList() updateState() } } data class WallUiState( val inProgressWall: List<Offset>, val cityWall: CityWall ) data class CityWall(val vertexes: List<Offset>)
同时需要将MainScreen中drawInProgressWall的参数改为List<Offset>,适配不可变列表的变化。
内容的提问来源于stack exchange,提问作者Black Spike
相关产品推荐
相关产品推荐

