```state.myList.forEachIndexed { index, it -> ...
# coroutines
c
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
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
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
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
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
Do I need that cocourine scope acround the forEach/map?
m
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
oooh. so I don't need the joinAll after all?
m
no joinAll.
c
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
Yes
c
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
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
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
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
Thanks!
f
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
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
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
launch
and
joinAll
would the the same. Not much point in doing
async
if you are not using the returned value.
c
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
that would mean there are still jobs running
c
actually. i spoke too soon.
m
or a job failed
c
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
happens all too often, I've resorted to building on the command line because I was tired of that issue
c
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
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
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
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
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
yes, like in so many other cases, there is more than one way to skin this cat
c
🙃 this is the only thing i hate about programming. sometimes im just like "but what is the right way" hahaha
a
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
Agreed! Nested viewModelScope.launch's make me cringe!