Sunday 7 May 2023

Recomposing 2d array element when its variable changes in Compose with Clean Architecture

I'm trying to achieve a fast image recomposition on a board when at least one variable from the objects in a 2d array changes.

I had a 2d array implemented as Array<Array<Cell>>, yet, to achieve recomposition, I created a duplicate remember variable on the Screen. This worked fast enough, but it resulted in duplication of code from the business logic of the application and did not allow us to use all the functions, so it was not the right way.

Structure of date class Cell:

data class Cell(
    val x: Int,
    val y: Int,
    var isOpen: Boolean = false,
    var index: Int = 0
)

Screen code:

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]) }  //duplicate remember variable

                        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) // duplication of code
                                board[i][j].isOpen = true                                 // from the business logic of the application
                            }
                        )
                    }
                }
            }
        }
    }
}

ViewModel code:

private val _board = mutableStateOf<Array<Array<Cell>>>(arrayOf())
val board: State<Array<Array<Cell>>> = _board

fun start() {
    _board.value = createBoard()// Use case that returns Array(row) { i -> Array(col) { j -> Cell(i, j) } }
}

After that, I added a duplicate 2d array Array<Array<MutableState<Cell>>> to the ViewModel. Yet, to update it, I had to add two double loops — one to convert Array<Array<Cell>> to Array<Array<MutableState<Cell>>> and the other to convert Array<Array<MutableState<Cell>>> back to Array<Array<Cell>>. This helped to get rid of duplicate code on the Screen and allowed us to implement all the functions, but predictably greatly increased the processing time for clicks.

Screen code:

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 code:

fun updateBoard(cell: Cell? = null, event: Event? = null) {
    val newBoard = boardState.map { row -> row.map { item -> item.value }.toTypedArray() }.toTypedArray() // one double loops
    _board.value = updateBoard(newBoard, cell, event) // Use case that returns Array(row) { i -> Array(col) { j -> Cell(i, j) } }
    boardState = board.value.map { row -> row.map { item -> mutableStateOf(item) }.toTypedArray() }.toTypedArray() // two double loops
}

Question: How can I achieve fast recomposition when changing the variable of any object in a 2d array?

Note: In my project, I follow the principles of Clean Architecture, so I cannot use Array<Array<MutableState<Cell>>> at the domain level, because MutableState is part of the androidx library.

Also, I want to add that when you click on a 2d field cell, other cells can change, and not just the one that was clicked.

Perhaps this can be solved using MutableStateFlow, however, unlike MutableState, it does not cause elements to be recomposed. Maybe there are other ways that I don't know about.

Update: I tried to cast Array<Array<Cell>> to Array<Array<MutableStateFlow<Cell>>>, and convert StateFlow to Compose State in Screen using collectAsState(), however this results in an error.

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() } // error
                            ...

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)
}


from Recomposing 2d array element when its variable changes in Compose with Clean Architecture

No comments:

Post a Comment