https://kotlinlang.org logo
c

Colton Idle

08/31/2022, 5:35 PM
Copy code
state.myList.forEachIndexed { index, it ->
  viewModelScope.launch {
    it.isDone.value = apiService.isDone(it.id)
  }
}
I thought I'd be able to add a
.await()
to the end of the launch block to know when all of those parrallel api calls are done, but that doesn't compile. Is there a more idiomatic way to do what I'm trying to do?
m

mkrussel

08/31/2022, 5:43 PM
You could use map instead of forEach to get a list of Jobs and then use
joinAll
. https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/join-all.html
e

Eric Chee

08/31/2022, 5:43 PM
I believe you can wrap all those calls into a List of defers and then call awaitAll on the list in a coroutine context
c

Colton Idle

08/31/2022, 5:51 PM
Something like this?
Copy code
viewModelScope.launch {
  state.myList.map { it ->
    val job = viewModelScope.launch {
      it.isDone.value = apiService.isDone(it.id)
    }
    job
  }.joinAll()

  //Do my thing that can only happen after all are done
}
Not sure if its idiomatic (because of the two viewModelScope.launch calls, but I think it should work!
m

mkrussel

08/31/2022, 5:54 PM
If you are already in a launch, then easiest way is to use
coroutineScope
.
Copy code
viewModelScope.launch {
  coroutineScope {
     state.myList.forEachIndexed { index, it ->
         launch {
             it.isDone.value = apiService.isDone(it.id)
         }
     }
   }
}
c

Colton Idle

08/31/2022, 5:58 PM
Do I need that cocourine scope acround the forEach/map?
m

mkrussel

08/31/2022, 5:59 PM
The coroutineScope function call will suspend until all jobs launched inside of it are completed. So it makes the outer launch wait for all the inner jobs to complete.
c

Colton Idle

08/31/2022, 6:00 PM
oooh. so I don't need the joinAll after all?
m

mkrussel

08/31/2022, 6:00 PM
no joinAll.
c

Colton Idle

08/31/2022, 6:01 PM
So I could do
Copy code
viewModelScope.launch {
  coroutineScope {
     state.myList.forEachIndexed { index, it ->
         launch {
             it.isDone.value = apiService.isDone(it.id)
         }
     }
   }
  //Do some work that requires all of the above to be completed first
}
m

mkrussel

08/31/2022, 6:01 PM
Yes
c

Colton Idle

08/31/2022, 6:01 PM
Any reason to use that vs this
Copy code
viewModelScope.launch {
  state.myList.map { it ->
    val job = viewModelScope.launch {
      it.isDone.value = apiService.isDone(it.id)
    }
    job
  }.joinAll()

  //Do my thing that can only happen after all are done
}
I guess the one with coroutineScope{} is actually more idiomatic since it plays within the bounds of the coroutine paragigm? 🤷
m

mkrussel

08/31/2022, 6:03 PM
Using
coroutineScope
creates a structured relationship between the inner jobs and the outer job. With the
joinAll
and multiple uses of the
viewModelScope
cancelling the code joining will not cancel the calls to
isDone
or the updating of
isDone.value
.
c

Colton Idle

08/31/2022, 6:05 PM
Interesting! Thanks for teaching! So in this case, if I use the structured relationship route, then what happens if one of the launches, fails? does the entire thing get cancelled?
coroutineScope {}, launch {}, async {}, all make my head spin 😄
i feel like i never really know when to use which.
m

mkrussel

08/31/2022, 6:07 PM
be default they will all get cancelled if one fails. You can use SupervisorJobs to prevent that or handle the exceptions in the launch block.
c

Colton Idle

08/31/2022, 6:08 PM
Thanks!
f

Francesc

08/31/2022, 6:28 PM
the
coroutineScope
approach is likely the simplest and most readable, but you could also use
async
instead to map the list to a a list of Deferred and then use
awaitAll
on that list
c

Colton Idle

08/31/2022, 6:32 PM
I originally only knew of await() and so thats what I was trying to do at first, but the coroutineScope approach does make sense. ANd for structured concurreny i think it also makes sense
f

Francesc

08/31/2022, 6:33 PM
that's the
async
alternative
Copy code
val deferred = state.myList.map { it ->
            viewmodelScope.async {
                it.isDone.value = apiService.isDone(it.id)
            }
        }

        deferred.awaitAll()
m

mkrussel

08/31/2022, 6:34 PM
launch
and
joinAll
would the the same. Not much point in doing
async
if you are not using the returned value.
c

Colton Idle

08/31/2022, 6:38 PM
Hm. This didn't seem to work at runtime.
Copy code
viewModelScope.launch {
  coroutineScope {
     state.myList.forEachIndexed { index, it ->
         launch {
             it.isDone.value = apiService.isDone(it.id)
         }
     }
   }
  //Do some work that requires all of the above to be completed first
}
the //Do some work, didn't get called. hm.
f

Francesc

08/31/2022, 6:39 PM
that would mean there are still jobs running
c

Colton Idle

08/31/2022, 6:39 PM
actually. i spoke too soon.
m

mkrussel

08/31/2022, 6:39 PM
or a job failed
c

Colton Idle

08/31/2022, 6:39 PM
android studio didn't deploy changes.
ive filed like 3 bugs in the past 6 months about this. they keep fixing different bugs though. gonna uninstall my app and try again
f

Francesc

08/31/2022, 6:40 PM
happens all too often, I've resorted to building on the command line because I was tired of that issue
c

Colton Idle

08/31/2022, 6:56 PM
Copy code
viewModelScope.launch {
  val deferred = state.myList.mapEach { index, it ->
    async {
      it.isDone.value = apiService.isDone(it.id)
    }
  }
  deferred.awaitAll()
  //Do some work that requires all of the above to be completed first
}
@Francesc so this would be your approach correct? I have to wrap the whole thing in a scope.launch{} I believe?
f

Francesc

08/31/2022, 6:57 PM
that's an option, yes. You need the
launch
for the
awaitAll
only, you don't need it for the
async
call. And doesn't look like you use the
index
so use
map
instead of
mapEach
. But for your use case,
coroutineScope
is simpler
c

Colton Idle

08/31/2022, 7:18 PM
I actually feel like "awaitAll" approach is easier, but only because thats what I was reaching for initially. Is there one of these 3 approaches that works better or more idiomatically? I know @mkrussel said
Using
coroutineScope
creates a structured relationship between the inner jobs and the outer job. With the
joinAll
and multiple uses of the
viewModelScope
cancelling the code joining will not cancel the calls to
isDone
or the updating of
isDone.value
.
Any update to your opinion mkrussel? I know its probably vauge. but yeah. with like 3 options. all of em seem pretty darn good. I do like a structured relationship tho 😄
f

Francesc

08/31/2022, 7:22 PM
it all depends on your use case.
launch
is for "fire and forget",
async
is for when you need the result of your task, and
coroutineScope
is helpful when you want to wait for all the children to finish or cancel all when one fails
note that
awaitAll
will also fail if any child fails
c

Colton Idle

08/31/2022, 7:29 PM
So in this case. CoroutineScope seems to make sense for me. Thanks! I guess none of them are necessarily "wrong", right. they all work. so yeah. coroutineScope makes sense to me.
f

Francesc

08/31/2022, 7:30 PM
yes, like in so many other cases, there is more than one way to skin this cat
c

Colton Idle

08/31/2022, 7:31 PM
🙃 this is the only thing i hate about programming. sometimes im just like "but what is the right way" hahaha
a

Alex Vanyo

08/31/2022, 7:56 PM
Of the options listed, I think the one that looks like
Copy code
viewModelScope.launch {
    // ...
    viewModelScope.launch {
        
    }
}
where there are nested
launch
on some external scope is the most “wrong” in the sense that it starts breaking down structured concurrency in harder to understand ways. Fire and forget is sometimes what you want, but that makes it more difficult to reason about ordering and cancellation.
c

Colton Idle

08/31/2022, 8:24 PM
Agreed! Nested viewModelScope.launch's make me cringe!
13 Views