Thread
#compose
    Mehdi Haghgoo

    Mehdi Haghgoo

    1 year ago
    I think I have made a horrible mistake when trying to read a state from Room-backed ViewModel. Error: java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied. Code follows in thread.
    This composable is responsible to show the details of the item pointed to by id.
    @Composable
    fun Detail(id: Int, viewModel: CostViewModel){
        var cost by mutableStateOf(Cost(costName =  "sample",  costValue = 10.0))
        GlobalScope.launch{cost = viewModel.findCost(id)}
        Surface(modifier = Modifier.shadow(4.dp)) {
            Column {
                BasicText(stringResource(id = R.string.prodcuct_details))
                BasicText("Cost: ${cost.costName}")
                BasicText("Value: ${cost.costValue}")
                Image(
                    painterResource(id = getRandomImageResource()),
                    contentDescription = cost.costName
                )
            }
        }
    }
    j

    jaqxues

    1 year ago
    Add remember:
    remember { mutableStateOf() }
    Mehdi Haghgoo

    Mehdi Haghgoo

    1 year ago
    Here's the full error:
    FATAL EXCEPTION: DefaultDispatcher-worker-1
    Process: <http://com.example.app|com.example.app>, PID: 19134
    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:1688)
    at androidx.compose.runtime.snapshots.SnapshotKt.current(Snapshot.kt:1864)
    at androidx.compose.runtime.SnapshotMutableStateImpl.setValue(MutableState.kt:324)
    at com.example.app.MainActivityKt.Detail$lambda-19(MainActivity.kt:550)
    at com.example.app.MainActivityKt.access$Detail$lambda-19(MainActivity.kt:1)
    at com.example.app.MainActivityKt$Detail$1.invokeSuspend(MainActivity.kt:324)
    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:738)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
    remember does not help. The same error.
    j

    jaqxues

    1 year ago
    You should be using it anyway, otherways you create a new mutableState for every single recomposition
    Mehdi Haghgoo

    Mehdi Haghgoo

    1 year ago
    How do you search an item from viewModel. Do I have to make
    findCost()
    return LiveData?
    j

    jaqxues

    1 year ago
    Wym search an item from viewModel? You can return anything you want. From your current usage, it looks like (suspend)
    findCost
    just returns a Cost object (which is fine). Although I would not use GlobalScope, but a
    rememberCoroutineScope()
    or maybe even sth like a
    LaunchedEffect()
    Besides that, I do not really see immediately what else is wrong with your code
    So I suspect that the MutableState.value = xx is not thread safe
    GlobalScope.launch {
        val tmp = viewModel.findCost(id)
        withContext(Dispatchers.Main) {
            cost = tmp
        }
    }
    Maybe this will help, but again, not sure
    Mehdi Haghgoo

    Mehdi Haghgoo

    1 year ago
    This is damn hard 😞
    Yes @jaqxues your snippet worked. Why?
    j

    jaqxues

    1 year ago
    Well if it works, then setting a state is not thread-safe. This means that if you call it on the wrong thread, it will basically not update immediately/correctly on the main thread which handles the UI and hence cause some errors. So the code snippet above first switches to the Main thread, then updates the State. When the UI is needed, the State is updated as expected and does not cause issues like this. I'm quite surprised that it isnt thread-safe though, don't worry, most things aren't this hard😃
    also, think of a
    Composable
    function as sth that will be executed over and over again (not under your control). When not using remember, you create a new mutableState every single time the composable is refreshed. Similarly,
    GlobalScope.launch
    is terrible since it will launch new coroutines every time it refreshes. You should not assume anything about recomposition. Make your code safe to be executed often, and let compose handle the rest. I would use sth like
    LaunchedEffect(id) { 
        // your coroutine code
    }
    Mehdi Haghgoo

    Mehdi Haghgoo

    1 year ago
    Thank you @jaqxues. So, LaunchedEffect is a Compose-friendly coroutine?
    Adam Powell

    Adam Powell

    1 year ago
    It's a composition-controlled coroutine launch, yes. You should never use
    CoroutineScope.launch
    from the body of a composable function. (Note that this is distinct from using
    launch
    in an event handler created in a composable function.)
    Adam Powell

    Adam Powell

    1 year ago
    As for the thread-safety aspects of snapshot state, what you've encountered is the result of snapshots being transactional.
    When a snapshot is taken (and composition does this for you under the hood) the currently active snapshot is thread-local. Everything that happens in composition is part of this transaction, and that transaction hasn't committed yet.
    So when you create a new
    mutableStateOf
    in composition and then pass it to another thread, as the
    GlobalScope.launch
    in the problem snippet does, you've essentially let a reference to snapshot state that doesn't exist yet escape from the transaction.
    This doesn't happen for snapshot state created as part of the "global transaction" - if you're not in an explicitly created snapshot, you're in the global transaction and any reads or writes are valid; they just aren't consistent with any particular snapshot. From this state they behave just like any other object read/write.
    The reason
    LaunchedEffect
    doesn't experience any issues around any of this is because
    LaunchedEffect
    doesn't actually launch a coroutine until after the composition has successfully completed and applied - after the composition snapshot commits successfully.
    (At a high level, we should probably have a lint warning around CoroutineScope.launch/async called from a composable function that warns that you really want to think about the problem in a different way, ideally linking to some documentation.)
    Again though, this is just fine:
    val scope = rememberCoroutineScope()
    
    Button(onClick = { scope.launch { doSuspendingThings() } }) {
    because the launch is part of the click handler, not part of the composable function.
    Mehdi Haghgoo

    Mehdi Haghgoo

    1 year ago
    So, what is the best way to use the result of a coroutine in a composable? Tried moving the UI code inside the coroutine, but it does not work!
    Even when I access suspending Room functionality inside coroutine, it throws error that I "Cannot access database on the main thread since it may potentially lock the UI for a long period of time."
    Adam Powell

    Adam Powell

    1 year ago
    var cost by remember { mutableStateOf(Cost(costName =  "sample",  costValue = 10.0)) }
    LaunchedEffect(id) {
        cost = viewModel.findCost(id)
    }
    LaunchedEffect
    runs on the composition's effect context, i.e. the UI thread. You can use
    withContext
    to switch dispatchers as usual.
    Mehdi Haghgoo

    Mehdi Haghgoo

    1 year ago
    Finally solved it this way:
    val cost by viewModel.findCost(id).collectAsState(Cost(costName = "default", costValue = 1.0))
    Surface(modifier = Modifier.shadow(4.dp)) {
        Column {
            BasicText(stringResource(id = R.string.prodcuct_details))
            BasicText("Cost: ${cost.costName}")
            BasicText("Value: ${cost.costValue}")
            Image(
                painterResource(id = getRandomImageResource()),
                contentDescription = cost.costName
            )
        }
    }
    Adam Powell

    Adam Powell

    1 year ago
    If
    findCost(id)
    doesn't return the same flow object instance every time for the same id you'll likely want to spell that as
    val cost by remember(viewModel, id) {
      viewModel.findCost(id)
    }.collectAsState(...)
    Without the remember or some other assurance that same id+viewmodel = same flow instance,
    collectAsState
    will cancel the old collect and start a new one every time that recomposes