Home > front end >  Reverse & shuffle mutableStateListOf()
Reverse & shuffle mutableStateListOf()

Time:02-15

itemList.reverse()

itemList is mutableStateListOf() object inside viewModel, above line throws below given exception:

java.util.ConcurrentModificationException
    at androidx.compose.runtime.snapshots.StateListIterator.validateModification(SnapshotStateList.kt:278)
    at androidx.compose.runtime.snapshots.StateListIterator.set(SnapshotStateList.kt:271)
    at java.util.Collections.reverse(Collections.java:435)
    at kotlin.collections.CollectionsKt___CollectionsJvmKt.reverse(_CollectionsJvm.kt:43)
    at in.rachika.composetest2.Tests.LazyColumnHeaderTest$MainScreenWithChildList$1$2.invoke(LazyColumnHeaderTest.kt:114)
    at in.rachika.composetest2.Tests.LazyColumnHeaderTest$MainScreenWithChildList$1$2.invoke(LazyColumnHeaderTest.kt:111)
    

I am unable to figure out how to reverse or shuffle a mutableStateListOf() Object

reverse() works well in isolated situation but my LazyColumn has stickyHeader() and SwipeToDismiss(), don't know may be that is creating problem.

Model

data class TestModel(
val isHeader: Boolean,
val UniqueKey: UUID,
val GroupId: UUID,
val GroupName: String,
val ItemName: String,
val children: MutableList<TestModel>,
var isExpanded: Boolean = false)

ViewModel

class TestViewModel: ViewModel() {
var itemList = mutableStateListOf<TestModel>()

init {
    viewModelScope.launch(Dispatchers.IO){
        loadList()
    }
}

private fun loadList() {
    for(i in 0..20){
        val groupName = "${i   1}. STICKY HEADER #"
        val groupUUID = UUID.randomUUID()

        val childList = mutableListOf<TestModel>()
        for(t in 0..Random.nextInt(10, 20)){
            childList.add(TestModel(
                isHeader = false,
                UniqueKey = UUID.randomUUID(),
                GroupId = groupUUID,
                GroupName = groupName,
                ItemName = "${t   1}. This is an CHILD ITEM... #${i   1} - ${Random.nextInt(1001, 5001)}",
                children = ArrayList()
            )
            )
        }

        viewModelScope.launch(Dispatchers.Main){
            itemList.add(TestModel(
                isHeader = true,
                UniqueKey = UUID.randomUUID(),
                GroupId = groupUUID,
                GroupName = groupName,
                ItemName = "",
                children = childList
            ))
        }
    }
}

fun addChildren(testModel: TestModel, onCompleted: (startIndex: Int) -> Unit){
    if(testModel.children.count() > 0){
        var index = itemList.indexOf(testModel)
        testModel.children.forEach { tItem ->
            itemList.add(index   1, tItem)
            index  
        }
        testModel.apply {
            isExpanded = true
            children.clear()
        }
        onCompleted(index)
    }
}

fun removeChildren(testModel: TestModel, onCompleted: (startIndex: Int) -> Unit){
    val startIndex = itemList.indexOf(testModel)   1
    while (startIndex < itemList.size && !itemList[startIndex].isHeader){
        testModel.children.add(itemList.removeAt(startIndex))
    }
    if(testModel.children.count() > 0){
        testModel.isExpanded = false
        onCompleted(startIndex - 1)
    }
}}

Composable functions

@Composable
fun MainScreenWithChildList(testViewModel: TestViewModel = viewModel()) {
    val lazyColumnState = rememberLazyListState()
    var reverseList by remember { mutableStateOf(false) }
    if (reverseList) {
        LaunchedEffect(Unit) {
            delay(1000)
            testViewModel.itemList.reverse()
        }
    }

    Box(modifier = Modifier.fillMaxSize()) {
        LazyColumn(
            state = lazyColumnState,
            modifier = Modifier.fillMaxSize()
        ) {
            testViewModel.itemList.forEach { testModel ->
                when (testModel.isHeader) {
                    true -> {
                        stickyHeader(key = testModel.UniqueKey) {
                            HeaderLayout(testModel = testModel) { testModel ->
                                if (testModel.isExpanded) {
                                    testViewModel.removeChildren(testModel) {}
                                } else {
                                    testViewModel.addChildren(testModel) {}
                                }
                            }
                        }
                    }
                    false -> {
                        item(key = testModel.UniqueKey) {
                            val dismissState = rememberDismissState()
                            if (dismissState.isDismissed(DismissDirection.EndToStart) || dismissState.isDismissed(
                                    DismissDirection.StartToEnd
                                )
                            ) {
                                if (dismissState.currentValue != DismissValue.Default) {
                                    LaunchedEffect(Unit) {
                                        dismissState.reset()
                                    }
                                }
                            }

                            SwipeToDismiss(
                                state = dismissState,
                                directions = setOf(
                                    DismissDirection.StartToEnd,
                                    DismissDirection.EndToStart
                                ),
                                dismissThresholds = { direction ->
                                    FractionalThreshold(if (direction == DismissDirection.StartToEnd || direction == DismissDirection.EndToStart) 0.25f else 0.5f)
                                },
                                background = { SwipedItemBackground(dismissState = dismissState) },
                                dismissContent = {
                                    ItemScreen(
                                        modifier = Modifier
                                            .fillMaxWidth()
                                            .animateItemPlacement(animationSpec = tween(600)),
                                        elevation = if (dismissState.dismissDirection != null) 16 else 0,
                                        testModel = testModel
                                    )
                                }
                            )
                        }
                    }
                }
            }
        }

        Button(
            onClick = {
                reverseList = true
            },
            modifier = Modifier.align(Alignment.BottomCenter)
        ) {
            Text(text = "Reverse")
        }
    }
}

@Composable
fun ItemScreen(modifier: Modifier, elevation: Int, testModel: TestModel) {
    Card(
        modifier = modifier,
        //elevation = animateDpAsState(elevation.dp).value
    ) {
        Text(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 16.dp, vertical = 8.dp),
            text = testModel.ItemName   "  ="
        )
    }
}

@Composable
fun SwipedItemBackground(dismissState: DismissState) {
    val direction = dismissState.dismissDirection ?: return
    val color by animateColorAsState(
        targetValue = when (dismissState.targetValue) {
            DismissValue.Default -> Color.LightGray
            DismissValue.DismissedToEnd -> Color.Green
            DismissValue.DismissedToStart -> Color.Red
        }
    )
    val icon = when (direction) {
        DismissDirection.StartToEnd -> Icons.Default.Done
        DismissDirection.EndToStart -> Icons.Default.Delete
    }
    val scale by animateFloatAsState(
        if (dismissState.targetValue == DismissValue.Default) 0.75f else 1.5f
    )
    val alignment = when (direction) {
        DismissDirection.StartToEnd -> Alignment.CenterStart
        DismissDirection.EndToStart -> Alignment.CenterEnd
    }
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(color)
            .padding(start = 12.dp, end = 12.dp),
        contentAlignment = alignment
    ) {
        Icon(icon, contentDescription = "Icon", modifier = Modifier.scale(scale))
    }
}

@Composable
fun HeaderLayout(testModel: TestModel, onExpandClicked: (testModel: TestModel) -> Unit) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .background(
                color = Color.LightGray,
                shape = RoundedCornerShape(8.dp)
            )
            .padding(horizontal = 32.dp, vertical = 16.dp),
        horizontalArrangement = SpaceBetween

    ) {
        Text(text = testModel.GroupName)
        TextButton(onClick = { onExpandClicked(testModel) }) {
            Text(text = if (testModel.isExpanded) "Collapse" else "Expand")
        }
    }
}

Above is the complete reproducible code. Please copy paste and try

CodePudding user response:

The reverse seems to work fine when the list size is small, but crashes when the number of items is large. I was able to reproduce this with the following MRE:

val list = remember { (1..100).toList().toMutableStateList() }
LaunchedEffect(Unit) {
    delay(1.seconds)
    list.reverse()
}
Text(list.toList().toString())

And reported this to compose issue tracker, star it so it can be solved faster.

Until then, you can reverse it manually as follows:

val newItems = list.reversed()
list.clear()
list.addAll(newItems)
  • Related