0

I am facing an issue wherein when updating stateFlow value in viewmodel from multiple threads, for some of the value I am not getting callback in UI. As an experiment I have tried the following

Below is the viewmodel code.

class MainViewModel : ViewModel() {

    private val _value = MutableStateFlow(-1)
    val value = _value.asStateFlow()

    var count = 0

    fun startUpdate() {
        viewModelScope.launch(Dispatchers.IO) {
            async {
                updateFrom1()
            }
            async {
                updateFrom1000()
            }
        }
    }

    fun updateValue(value: Int) {
        _value.update { value }
    }

    suspend fun updateFrom1() = withContext(Dispatchers.IO) {
        for (i in 1..1000) {
            updateValue(i)
            Log.d("Ujjwal", "UpdateFrom1 :$i")
        }
    }

    suspend fun updateFrom1000() = withContext(Dispatchers.IO) {
        for (i in 1000 downTo 1) {
            updateValue(i)
            Log.d("Ujjwal", "UpdateFrom1000 :$i")
        }
    }
}

And here is my activity code

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewmodel = MainViewModel()
        setContent {
            MyApplicationTheme {
                // A surface container using the 'background' color from the theme
                MyFunction(viewmodel)
            }
        }
    }
}

@Composable
fun MyFunction(viewModel: MainViewModel) {
    val value by viewModel.value.collectAsState()

    when (value) {
        else -> {
            viewModel.count = viewModel.count + 1
            Text(text = "value is ${viewModel.count}")
            Log.d("Ujjwal", "count is : ${viewModel.count}")
        }
    }

    LaunchedEffect(Unit ){
        viewModel.startUpdate()
    }
}

My expectation is at the end I want the value of count to be 2000 (That is it should get executed for all the updates of _value)

For the function I have tried the following variant, but none of them have worked

fun updateValue(value: Int) {
        _value.update { value }
    }

fun updateValue(value: Int) {
        viewModelScope.launch {
            _value.update { value }
        }
    }
    
fun updateValue(value: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            _value.update { value }
        }
    }

fun updateValue(value: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            _value.value = value
        }
    }

fun updateValue(value: Int) {
        viewModelScope.launch {
            _value.value = value
        }
    }

fun updateValue(value: Int) {
        viewModelScope.launch {
            _value.emit(value)
        }
    }

fun updateValue(value: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            _value.emit(value)
        }
    }

suspend fun updateValue(value: Int) = withContext(Dispatchers.IO) {
        _value.emit(value)
    }

suspend fun updateValue(value: Int) = withContext(Dispatchers.IO) {
        _value.value = value
    }

fun updateValue(value: Int) {
       viewModelScope.launch {
           mutex.withLock {
               _value.value = value
           }
       }
   }

Is there anything that I am missing or having some wrong assumption of how stateFlow is supposed to behave ?
For each execution, I get different value of count (usually around 8-14)

  • 1
    The UI updates on the frame clock, 16ms for 60Hz, while your coroutines loop unbounded, so this is expected. If you add a delay in your loops so that the UI can catch-up, you will get all the updates. – Francesc Nov 18 '22 at 17:13
  • @Francesc I am fine even if UI doesn't update. But the value of `count` in viewmodel should get updated to 2000 right ? – Ujjwal Kumar Maharana Nov 18 '22 at 17:18
  • Your value will be updated 2000 times, but you have 2 competing coroutines, one racing to 1000 and the other racing to 0, what value you end up with depends on which coroutine finishes last. Why do you expect the coroutine going up to 1000 to always win? They run in parallel. – Francesc Nov 18 '22 at 17:22
  • So for each coroutine, it changes the value of `value` variable right ? And for each change in value of `value` variable, a callback should ideally go to activity where I am collectingAsState. And for each callback that I receive I update the value of `count` in viewmodel by 1. So all in all there should be 2000 callback & value of `count` at end should be 2000 – Ujjwal Kumar Maharana Nov 18 '22 at 17:23
  • @Francesc Even if we assume that `_value` is missing value because of multiple threads accessing at the same time. I even tried with `.update{}` or by using `mutex` which as per documentation helps in making it thread safe. But even after that there is no difference in the outcome – Ujjwal Kumar Maharana Nov 18 '22 at 17:27
  • No, your activity is not collecting unbounded, it follows the frame clock so you will miss updates. Try it with a delay in your loops and then you'll see all the updates being collected. – Francesc Nov 18 '22 at 17:27
  • You're not actually collecting values emitted by the flow. Only thing that is happening when you [`collectAsState`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#(kotlinx.coroutines.flow.Flow).collectAsState(kotlin.Any,kotlin.coroutines.CoroutineContext)) is you trigger a recomposition when `value` changes and you get to pickup the most recent value. And as the first comment mentioned you can recompose at most once per frame, that's why you only get a handful of updates before your loops finish. – Pawel Nov 18 '22 at 17:33
  • @Pawel If that is the case, then even in normal scenario lets say from 2 different thread, a stateflow value was updated at the exact moment. Then in that case we are bound to loose the event. Is there any way to capture those events. Most importantly to capture the latest value atleast (the last value) – Ujjwal Kumar Maharana Nov 18 '22 at 17:34

0 Answers0