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

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
  • 点击单元格时,可能同时改变其他多个单元格的状态

解决方案

核心思路

  1. 领域层保持纯净:使用不可变的data class Cell(将var改为val,修改时通过copy()生成新对象),完全不依赖androidx库。
  2. ViewModel层用StateFlow管理全局棋盘状态:将领域层返回的Array<Array<Cell>>包装为MutableStateFlow,更新时生成新的数组(仅替换变化的Cell)并发射。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 20:07:04