Compose结合Clean Architecture实现二维数组元素变更快速重组
在Jetpack Compose结合Clean Architecture实现棋盘单元格变更的快速重组
需求:实现当二维数组中Cell对象的任意变量发生变化时,棋盘上对应的图片快速重组,同时遵循Clean Architecture原则。
尝试过的方案
方案1:Screen层为每个Cell创建remember的MutableState
Cell数据类:
data class Cell( val x: Int, val y: Int, var isOpen: Boolean = false, var index: Int = 0 )
Screen层代码:
val row = 5 val col = 5 val board = viewModel.board.value Column(Modifier.verticalScroll(rememberScrollState())) { Row(Modifier.horizontalScroll(rememberScrollState())) { Column { for (i in 0 until row) { Row { for (j in 0 until col) { val currentItem = remember(board) { mutableStateOf(board[i][j]) } Image( painter = painterResource( id = if (currentItem.value.isOpen) { when (currentItem.value.index) { 0 -> R.drawable.ic_num_0 1 -> R.drawable.ic_num_1 2 -> R.drawable.ic_num_3 else -> R.drawable.ic_field } } else { R.drawable.ic_field } ), contentDescription = null, modifier = Modifier.clickable { currentItem.value = currentItem.value.copy(isOpen = true) board[i][j].isOpen = true } ) } } } } } }
ViewModel层代码:
private val _board = mutableStateOf<Array<Array<Cell>>>(arrayOf()) val board: State<Array<Array<Cell>>> = _board fun start() { _board.value = createBoard() // 返回Array(row) { i -> Array(col) { j -> Cell(i, j) } } }
优缺点:重组速度达标,但Screen层代码冗余,业务逻辑重复,无法复用功能。
方案2:ViewModel维护双数组(普通数组+MutableState数组)
Screen层代码:
val row = 5 val col = 5 val board = viewModel.board.value Column(Modifier.verticalScroll(rememberScrollState())) { Row(Modifier.horizontalScroll(rememberScrollState())) { Column { for (i in 0 until row) { Row { for (j in 0 until col) { val currentItem = remember(board) { viewModel.boardState[i][j] } Image( painter = painterResource( id = if (currentItem.value.isOpen) { when (currentItem.value.index) { 0 -> R.drawable.ic_num_0 1 -> R.drawable.ic_num_1 2 -> R.drawable.ic_num_3 else -> R.drawable.ic_field } } else { R.drawable.ic_field } ), contentDescription = null, modifier = Modifier.combinedClickable( onClick = { viewModel.updateBoard(board[i][j], Event.OnClick) } ) ) } } } } } }
ViewModel层代码:
fun updateBoard(cell: Cell? = null, event: Event? = null) { val newBoard = boardState.map { row -> row.map { item -> item.value }.toTypedArray() }.toTypedArray() _board.value = updateBoard(newBoard, cell, event) // 返回更新后的Array<Array<Cell>> boardState = board.value.map { row -> row.map { item -> mutableStateOf(item) }.toTypedArray() }.toTypedArray() }
优缺点:消除了Screen层冗余代码,但更新时需要双层循环转换数组,导致点击处理耗时大幅增加。
方案3:尝试使用StateFlow数组(出现错误)
Screen层错误代码片段:
val board by viewModel.board Column(Modifier.verticalScroll(rememberScrollState())) { Row(Modifier.horizontalScroll(rememberScrollState())) { Column { for (i in 0 until row) { Row { for (j in 0 until col) { val currentItem = remember(board) { board[i][j].collectAsState() } // 此处错误 // ... } } } } } }
ViewModel层代码:
private val _board = mutableStateOf<Array<Array<MutableStateFlow<Cell>>>>(arrayOf()) val board: State<Array<Array<MutableStateFlow<Cell>>>> = _board fun updateBoard(cell: Cell? = null, event:Event? = null) { _board.value = updateBoard(board.value, cell, event) }
问题:Screen层中collectAsState()不能直接在remember块中调用,且这种设计会导致大量StateFlow实例,维护成本高。
约束条件
- 遵循Clean Architecture原则:领域层不能使用androidx库的
MutableState - 点击单元格时,可能同时改变其他多个单元格的状态
解决方案
核心思路
- 领域层保持纯净:使用不可变的
data class Cell(将var改为val,修改时通过copy()生成新对象),完全不依赖androidx库。 - ViewModel层用StateFlow管理全局棋盘状态:将领域层返回的
Array<Array<Cell>>包装为MutableStateFlow,更新时生成新的数组(仅替换变化的Cell)并发射。 - Screen层依赖StateFlow自动跟踪变化:通过
collectAsState()获取棋盘状态,Compose会自动检测数组中Cell对象的变化(因为data class的equals基于属性),仅重组变化的单元格。
具体实现
1. 领域层(纯净的Cell类)
// 领域层代码,无androidx依赖 data class Cell( val x: Int, val y: Int, val isOpen: Boolean = false, val index: Int = 0 )
2. ViewModel层
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch class BoardViewModel : ViewModel() { private val _board = MutableStateFlow<Array<Array<Cell>>>(emptyArray()) val board: StateFlow<Array<Array<Cell>>> = _board.asStateFlow() fun start(row: Int = 5, col: Int = 5) { // 从领域层获取初始棋盘 val initialBoard = Array(row) { i -> Array(col) { j -> Cell(i, j) } } _board.value = initialBoard } fun updateBoard(cell: Cell, event: Event) { viewModelScope.launch { // 复制原数组,避免修改原数据 val newBoard = _board.value.map { row -> row.copyOf() }.toTypedArray() // 根据事件更新对应Cell(示例:点击后打开单元格) val updatedCell = cell.copy(isOpen = true) newBoard[cell.x][cell.y] = updatedCell // 如果需要同时更新多个单元格,在此处添加逻辑 // 例如:打开相邻单元格 // updateAdjacentCells(newBoard, cell) _board.value = newBoard } } // 示例:批量更新相邻单元格逻辑 private fun updateAdjacentCells(board: Array<Array<Cell>>, cell: Cell) { // 实现相邻单元格更新逻辑,生成新的Cell对象替换原位置 } } sealed class Event { object OnClick : Event() }
3. Screen层
import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.horizontalScroll import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun BoardScreen() { val viewModel: BoardViewModel = viewModel() val board by viewModel.board.collectAsState() val row = board.size val col = if (row > 0) board[0].size else 0 Column(Modifier.verticalScroll(rememberScrollState())) { Row(Modifier.horizontalScroll(rememberScrollState())) { Column { for (i in 0 until row) { Row { for (j in 0 until col) { val cell = board[i][j] // 用cell作为remember的key,确保仅当cell变化时重组该Image val painter = remember(cell) { painterResource( id = if (cell.isOpen) { when (cell.index) { 0 -> R.drawable.ic_num_0 1 -> R.drawable.ic_num_1 2 -> R.drawable.ic_num_3 else -> R.drawable.ic_field } } else { R.drawable.ic_field } ) } Image( painter = painter, contentDescription = "Cell at ${cell.x}, ${cell.y}", modifier = Modifier.clickable { viewModel.updateBoard(cell, Event.OnClick) } ) } } } } } } }
方案优势
- 符合Clean Architecture:领域层无androidx依赖,ViewModel层仅负责状态管理,Screen层仅做UI渲染。
- 高效重组:仅当Cell对象变化时,对应的Image才会重组,避免全局刷新。
- 无冗余代码:业务逻辑集中在ViewModel层,Screen层代码简洁可复用。
- 支持批量更新:可在ViewModel中轻松实现同时修改多个单元格的逻辑,且仅发射一次新数组,避免多次重组。
内容的提问来源于stack exchange,提问作者Viktor Skliarov
相关产品推荐
相关产品推荐

