Search code examples
multithreadingasync-awaitkotlin-coroutineslauncheffect

I am confused with LauchedEffect and Coroutines or Threads in Kotlin. Are they run on top of the other?


In a Composable function every time a State variable is changed the function is run again top to bottom and all LaunchedEffect(Unit) are run again.

When I have a Coroutine inside the LaunchedEffect that routine will be run again. But what happens when the routine is ran as a thread?

Let say I have:

var res = 0
LaunchedEffect(Unit) {
  val job = scope.async(Dispatcher.IO) {
     res = getsomething() //Long process
  }
  job.await()
}

Will the getsomething() function run again all the times generating multiple threads or will be ignored if it is still running the first thread?

Let say I have:

var res = 0
LaunchedEffect(Unit) {
  val job = scope.launch(Dispatcher.IO) {
     res = getsomething() //Long process
  }
  job.join()
}

I guess is the same as the async but is it the same response?

Let say I have:

val res = suspend_get_other_something()

And in other Composable function something like this:

suspend fun suspend_get_other_something(): Int {

LaunchedEffect(Unit) {
    val res = scope.launch {
        MyViewModel.runStuff() 
        //Get Some Data from Internet. Internally it wont get more data until it is 
        //finished and emit the outcome received in the state and error variables bellow
    }
    res.join()
}

val state by MyViewModel.state.collectAsStateWithLifecycle()
val error by MyViewModel.error.collectAsStateWithLifecycle()

LaunchedEffect(state) {
    if (state == -1) return@LaunchedEffect
    else {
        MyViewModel.resetState()
        return state
    }
}
}

LaunchedEffect(error) {
    if (error == -1) return@LaunchedEffect
    else {
        MyViewModel.resetError()
        return error
    }
}

What happens in the case o a suspend function [suspend_get_other_something()], will it run the same instance and get the outcome of the runStuff() function or will it try to run another instance?


Solution

  • As broot correctly identified in the comments, the coroutine that LaunchedEffect launches is cancelled on recomposition when a new LaunchedEffect is created. It isn't, though, because a new LaunchedEffect is only started when its key changes. Since you use Unit as the key that will never change, and therefore the LaunchedEffect will not be restarted on recompositions. It is therefore only ever executed once. Only when the composable leaves the composition entirely and reenters later on (as it does on device reconfigurations like screen rotations, for example), the LaunchedEffect is cancelled and restarted.

    But you have another issue here: In the coroutine launched by LaunchedEffect you launch another coroutine (by async and launch), and that coroutine is not cancelled when the LaunchedEffect's coroutine would be cancelled. It is dependent on the scope and therefore is only cancelled when that is cancelled.

    That doesn't seem right, so you should replace your first two LaunchedEffects by this:

    LaunchedEffect(Unit) {
        withContext(Dispatchers.IO) {
            res = getsomething()
        }
    }
    

    That said, it smells fishy switching to the IO dispatcher in a composable. Your composables should only concern themselves with anything directly UI related. And the proper dispatcher for that is Dispatchers.Main, which the LaunchedEffect is already running on. If getsomething() should really be run on the IO dispatcher then your composables probably shouldn't even be calling it in the first place. That's where your view model comes into play: It outlives the UI and can handle everything that is IO related, like file system, network or database access.

    This also has the added benefit that the view model is decoupled from your composable's recompositions. So just launch a new coroutine there (in the viewModelScope) and call getsomething(). Either do it in the init block if it is a one-off operation or bind it to a user action, like a button click that calls a view model function that in turn launches the new coroutine for getsomething(). Just make sure to never expose suspend functions in your view model. That way you don't need a LaunchedEffect to access your view model in the first place.

    I'm afraid I didn't understand your last example as it doesn't compile, but I guess what I explained above should also help here. Justs make sure to handle all IO in the viewmodel and expose any state changes in your StateFlow. Controlling the IO can be initiated by the UI by calling the appropriate view model functions (runStuff, resetState, resetError), just make sure they do not suspend and don't have a return value. Their sole purpose is to actually do the dirty work and update your StateFlows as needed.