I encounter this pattern a lot ```viewModelScope....
# coroutines
c
I encounter this pattern a lot
Copy code
viewModelScope.launch {
 val thing: String = someSuspendFunctionThatReturnsAValue()
 aMethodCallThatIsAFlow(thing).collect {
   if (it.status == "SUCCESS"){
    state.success = true
    //should I call job.stop() here?
   }
 }
}
My problem is that I want the flowable to stop collecting. Can I just save a ref to the call to launch and then call stop()? Or is starting a flowable in a coroutine scope frowned upon in general?
a
Copy code
aMethodCallThatIsAFlow(thing).first { it.status == "SUCCESSFUL" }
state.success = true
?
(also returns the object that matched)
c
In my case, aMethodCallThatIsAFlow is a firebase firestore snapshot listener, where I'm listening for a document to be updated into the "success" state. I don't think first() would work here? I'm going to give it a shot though. Maybe first() would work if I wrapped it in a runBlocking?
Something is definitely funky with doing it my way because the second time I press my button (which triggers the vMS.launch{}, I get two Log statement prints inside of the if (status == "SUCCESS"), but the first time I land here I only get one log statement. So far though my code above "works", it just doesn't seem "right"
a
Why wouldn't first work? It's basically just a filter/take(1)/collect
c
Hm. It didn't work, for me. Let me paste what I had.
This is what I currently have. It "works"
Copy code
fun confirmEvent(successEvent: () -> Unit) {
  viewModelScope.launch {
    var response = repository.getIdFromServer()

    if (response.isValid) {
      var response2 = repository.getObjectWithCurrentStatus(response.data.id)

      //Keep listening to changes on the server until the status changes from IN_PROGRESS TO SUCCESS
      response2.collect {
        if (it.status != "IN_PROGRESS") {
          if (it.status == "SUCCESS") {
            successEvent()
          } else {
            state.errorDialog = true
            state.errorDialogText = "Error 2: " + it.status
          }
        }
      }
    } else {
      state.errorDialog = true
      state.errorDialogText = response.errorText
    }
  }
}
Switching over to
first
does not work. But maybe I did it wrong?
Copy code
fun confirmEvent(successEvent: () -> Unit) {
  viewModelScope.launch {
    var response = repository.getIdFromServer()

    if (response.isValid) {
      var response2 = repository.getObjectWithCurrentStatus(response.data.id)

      //Keep listening to changes on the server until the status changes from IN_PROGRESS TO SUCCESS
      val waiting = response2.first {(it.status == "SUCCESS")}
      
        if (waiting.status != "IN_PROGRESS") {
          if (waiting.status == "SUCCESS") {
            successEvent()
          } else {
            state.errorDialog = true
            state.errorDialogText = "Error 2: " + waiting.status
          }
        }
      }
    } else {
      state.errorDialog = true
      state.errorDialogText = response.errorText
    }
  }
}
@Adam Powell Actually wait. it seems to work now? I swear the first time I launched it it didn't work. I swear I just freakin hit this issue in AS again. https://issuetracker.google.com/issues/191997469 Lol. Anyway. Im going to run some more tests, but yes first{} seemed to have done exactly what I was expecting it to do. One last thing though (and i guess maybe its not worth the hypothetical question), but how would I do this if
first
didn't exist and
collect
is all I had?
Would i go the route that I said before of cancelling/stopping the coroutine scope?
I wonder if this would just be the right approach
Copy code
viewModelScope.launch {

        beaconService.streamTest().collect {

            //Do something then
            this.coroutineContext.job.cancel()

        }
}
f
The scope is the viewmodel's so you shouldn't cancel it, you should get the job and call cancel on that instead. Cancelling the scope would cancel all coroutines and once cancelled you can't launch again
I read also that is customary for functions that launch a coroutine and return immediately to be defined as extensions on coroutine scope
c
wait. cancelling the scope cancels all coroutines. interesting. @Fran so what you're saying is that
this.coroutineContext.job.cancel()
should work fine then? Because I'm cancelling the job? or should I do
Copy code
val job = viewModelScope.launch {

        beaconService.streamTest().collect {

            //Do something then
            job.cancel()

        }
}
f
Yes, but launch returns a job, you can get it there
Right
But the first operator is the right way for your scenario
c
So I can't reference the job from inside the job. so im kinda back at square 1. lol
tl;dr: throw
c
Interesting. Yeah, essentially I want to
return
out of the flow, but I guess
throw
is what I should use.
who knew this would get so confusing. Interestingly enough
this.coroutineContext.job.cancel()
did seem to work also.
a
it also has some rather nasty other side effects
c
oof. okay.
throw
it is.
a
all
cancel()
is doing is causing other suspend points and cancellation checks later to throw a
CancellationException
, and as the inner callee you don't know how broad that
job
is
if you consider what flows are and how they work, throw is the only thing that can do what you want here. Consider a flow that iterates over a collection:
Copy code
flow {
  for (item in list) {
    emit(item)
  }
}
how can something called by
emit
cause that
for
loop to stop iterating other than by throwing?
or add in resource management:
Copy code
flow {
  val thing = openThing()
  try {
    while (thing.hasMore()) {
      emit(thing.readMore())
    }
  } finally {
    thing.close()
  }
}
how do you make this code stop reading this resource before it's finished and properly clean up?
c
I guess using a standard language construct like throw makes sense when you put it that way. I guess I just associate throw with something scary lol and so it feels "wrong" but i appreciate you teaching me!
👍 1
a
yeah decades of, "don't use exceptions for control flow" beaten into your skull will do that 🙂
sort of a side rant but this sort of thing is why it makes me sad when people get religion about returning result types to the exclusion of throwing exceptions at all. There are good reasons to dislike APIs that use exceptions inappropriately but what they do is incredibly useful when you need to say, "I could not meet the contract of what you asked me to do" up layers of the call stack
c
funny you mention that, as thats an upcoming issue Im likely to post about in #getting-started
thank you adam! cheers
👍 1
a
https://www.artima.com/articles/the-trouble-with-checked-exceptions is a good piece of reading in the general space
n
FYI: cancel within launch does not cancel the parent scope
Copy code
viewModelScope.launch { // Receiver of lambda is CoroutineScope for just this launched Job
    this.coroutineContext.job.cancel() //Does not cancel viewModelScope, just cancels the launched Job
    cancel() // Or you can just call it on the scope :)
}
And if you need to terminate the flow early, but
first { ... }
isn't enough,
takeWhile { ... }
or
transformWhile { ... }
may be useful so you don't need to deal with the exceptions yourself.
a
cancel within launch does not cancel the parent scope
while this is technically correct, cancelling your current scope is very often something you do not want to do. Consider:
Copy code
suspend fun doAThing() {
  someFlow.collect {
    if (it % 2 == 0) coroutineContext.job.cancel()
  }
}

// ...
doAThing()
doSomethingElse() // does this run? does it finish its work? why or why not?