ViewModel: Error on loading long list in IO Dispatcher

Viewed 547

I want to load a long list inside my ViewModel class, queried from database. Data will then be shown using LazyColumn. When loading in IO Dispatcher using kotlin coroutine it throws exception but no problem when loading in Main Dispatcher.

ViewModel Class:

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

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

Error Message:

E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
Process: in.rachika.composetest2, PID: 19469
java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
    at androidx.compose.runtime.snapshots.SnapshotKt.readError(Snapshot.kt:1518)
    at androidx.compose.runtime.snapshots.SnapshotKt.current(Snapshot.kt:1758)
    at androidx.compose.runtime.snapshots.SnapshotStateList.add(SnapshotStateList.kt:374)
    at in.rachika.composetest2.Tests.ViewModel.TestViewModel.loadList(TestViewModel.kt:24)
    at in.rachika.composetest2.Tests.ViewModel.TestViewModel.access$loadList(TestViewModel.kt:13)
    at in.rachika.composetest2.Tests.ViewModel.TestViewModel$1.invokeSuspend(TestViewModel.kt:19)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
2 Answers

Snapshots are transactional and run on ui thread. You are attempting to create a snapshot on the IO thread. If you want to do your data loading on the IO thread, you need to switch back to the UI thread after you've retrieved the data. Example:

class MyViewModel: ViewModel() {
    fun getData() {
        viewModelScope.launch(Dispatchers.IO) {
            // Get Data
            val data = someAPI.getData()

            withContext(Dispatchers.Main) {
                // Display the data...
                displayData(data)
            }
        }
    }
}

Based on the suggestion made by @Johann I have solved my problem below is my updated code:

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

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

}

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

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

        //change the scope just before adding data class to mutableStateList
        //because mutableStateList automatically notifies LazyColumn about this data update.
        viewModelScope.launch(Dispatchers.Main){
            itemList.add(TestModel(
                isHeader = true,
                GroupId = UUID.randomUUID(),
                GroupName = groupName,
                ItemName = "",
                children = childList
            ))
        }
    }
}

Thanks @Johann, I am marking your answer as accepted.

Related