androidandroid-jetpack-compose

snapshotFlow thread-safety in Jetpack Compose


I'm going through Advanced State and Side Effects in Jetpack Compose course and I have this code in the end of this chapter.

@Composable
fun ToDestinationUserInput(onToDestinationChanged: (String) -> Unit) {
    val editableUserInputState = rememberEditableUserInputState(hint = "Choose Destination")

    ...

    val currentOnDestinationChanged by rememberUpdatedState(onToDestinationChanged)
    LaunchedEffect(editableUserInputState) {
        snapshotFlow { editableUserInputState.text }
            .filter { !editableUserInputState.isHint }
            .collect {
                currentOnDestinationChanged(editableUserInputState.text)
            }
    }
}

and editableUserInputState looks like this:

class EditableUserInputState(private val hint: String, initialText: String) {
    var text by mutableStateOf(initialText)

    val isHint: Boolean
        get() = text == hint

    ...
}

Is it true that after the first of two following lines some other thread can change editableUserInputState.text and filter part will check the updated value instead of the one we got in first line?

        snapshotFlow { editableUserInputState.text }
            .filter { !editableUserInputState.isHint }

Solution

  • Although Flows are thread-safe, this is not how flows should be used.

    The flow operations are influenced by several side effects which may actually make this prone to race conditions, potentially leading to wrong results. The basic issue is that the chain of flow operations is not guaranteed to be executed en bloc (i.e. as one, atomic operation). The flow operation's lambdas are suspend functions and have a suspension point. That means when one operation finishes, the current coroutine running the flow operations can be suspended and another coroutine may be executed instead.

    You don't have multiple threads involved (because the UI runs only on a single thread, the Main thread) so you don't have to worry about multiple coroutines running in parallel, but there is always the possibility the processing of the flow chain is paused after each operation because another coroutine now occipies the (one, single) thread.

    All of this wouldn't be an issue if the flow's lambdas wouldn't access shared mutable state1 located outside. Let's have a look:


    1 as in Shared mutable state, unrelated to the MutableState of Compose.

    2 Since the Material library is managed by the Compose BOM you would need to update that to at least 2024.11.00. At the time of writing that is still in alpha so you would need to change the Compose BOM in the module-level build.gradle to:

    def composeBom = platform('androidx.compose:compose-bom-alpha:2024.11.00')
    

    Note that the code lab at the time of writing still uses the old Material 2 library (androidx.compose.material:material). In a real application you would want to use the more modern Material 3 (androidx.compose.material3:material3).