Jetpack Compose lazy column all items recomposes when a single item update

MutableState works using structural equality which check if you update state.value with new instance. You are creating a new instance of your list on each time you select a new item.

You can use SnapshotStateList which triggers recomposition when you add, delete or update existing item with new instance. SnapshotStateList is a List which gets item with time O(1) complexity for getting an item with item[index] instead of iterating whole list with O(n) in worst case.

Using mutableStateListOf only

Result is only single item gets recomposed.

enter image description here

You can update your ViewModel with SnapshotState list as

class MyViewModel : ViewModel() {

    private val initialList = listOf(
        Person(id = 0, name = "Name0"),
        Person(id = 1, name = "Name1"),
        Person(id = 2, name = "Name2"),
        Person(id = 3, name = "Name3"),
        Person(id = 4, name = "Name4"),
        Person(id = 5, name = "Name5"),
        Person(id = 6, name = "Name6"),
    )

    val people = mutableStateListOf<Person>().apply {
        addAll(initialList)
    }

    fun toggleSelection(index: Int) {
        val item = people[index]
        val isSelected = item.isSelected
        people[index] = item.copy(isSelected = !isSelected)
    }
}

ListItem composable

@Composable
private fun ListItem(item: Person, onItemClick: (Int) -> Unit) {
    Column(
        modifier = Modifier.border(3.dp, randomColor())
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .clickable {
                    onItemClick(item.id)
                }
                .padding(8.dp)
        ) {
            Text("Index: Name ${item.name}", fontSize = 20.sp)
            if (item.isSelected) {
                Icon(
                    modifier = Modifier
                        .align(Alignment.CenterEnd)
                        .background(Color.Red, CircleShape),
                    imageVector = Icons.Default.Check,
                    contentDescription = "Selected",
                    tint = Color.Green,
                )
            }
        }
    }
}

Your list

@Composable
fun ListScreen(people: List<Person>, onItemClick: (Int) -> Unit) {
    LazyColumn(
        verticalArrangement = Arrangement.spacedBy(2.dp),
        modifier = Modifier.fillMaxSize()
    ) {

        items(items = people, key = { it.hashCode() }) {

            ListItem(item = it, onItemClick = onItemClick)
        }
    }
}

The code i use for visually checking recomposition

fun randomColor() = Color(
    Random.nextInt(256),
    Random.nextInt(256),
    Random.nextInt(256),
    alpha = 255
)

With ViewState

Result

enter image description here

sealed class ViewState {
    object Loading : ViewState()
    data class Success(val data: List<Person>) : ViewState()
}

And update ViewModel as

class MyViewModel : ViewModel() {

    private val initialList = listOf(
        Person(id = 0, name = "Name0"),
        Person(id = 1, name = "Name1"),
        Person(id = 2, name = "Name2"),
        Person(id = 3, name = "Name3"),
        Person(id = 4, name = "Name4"),
        Person(id = 5, name = "Name5"),
        Person(id = 6, name = "Name6"),
    )

    private val people: SnapshotStateList<Person> = mutableStateListOf<Person>()

    var viewState by mutableStateOf<ViewState>(ViewState.Loading)
        private set

    init {
        viewModelScope.launch {
            delay(1000)
            people.addAll(initialList)
            viewState = ViewState.Success(people)
        }
    }

    fun toggleSelection(index: Int) {
        val item = people[index]
        val isSelected = item.isSelected
        people[index] = item.copy(isSelected = !isSelected)
        viewState = ViewState.Success(people)
    }
}

1000 ms and delay is for demonstration. In real app you will get data from REST or db.

Screen that displays list or Loading using ViewState

@Composable
fun ListScreen(
    viewModel: MyViewModel,
    onItemClick: (Int) -> Unit
) {

    val state = viewModel.viewState
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        when (state) {
            is ViewState.Success -> {

                val people = state.data
                LazyColumn(
                    verticalArrangement = Arrangement.spacedBy(2.dp),
                    modifier = Modifier.fillMaxSize()
                ) {
                    items(items = people, key = { it.id }) {
                        ListItem(item = it, onItemClick = onItemClick)
                    }
                }
            }

            else -> {
                CircularProgressIndicator()
            }
        }
    }
}

Leave a Comment