0

I am subscribing to a Kotlin SharedFlow of booleans within a repeatOnLifecycle block in Android. I want to subscribe until I receive the first true boolean and act on it.

As soon as the first true boolean is received I need to unsubscribe and run the processing funtion within the lifecyce scope so it gets cancelled when the lifecycle transitions to an invalid state.

When I call cancel on the current scope and embed the processing code in a NonCancellable context it will not be cancelled on lifecycle events.

I think I would want something like a takeWhile inlcuding the first element that did not match the predicate.

Below are some sample flows where I want to collect all elements until the $ sign:

true $ ...
false, true $ ...
false, false, true $ ...

Sample code:

viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        flowOfInterest.collectLatest {
            if (it) {
                stopCollection()
                doOnTrue()
            } else {
                doOnFalse()
            }
        }
    }
}

What is the correct/simplest way to achieve this behavior?

Thanks!

metrik
  • 17
  • 1
  • 6

4 Answers4

1

You can use the first function to continue collecting until the given predicate returns true.

viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        flowOfInterest.first { 
            if(it) doOnTrue() else doOnFalse()
            it 
        }
    }
}
Arpit Shukla
  • 9,612
  • 1
  • 14
  • 40
  • Thank! I was not aware that first can also carry a predicate. I think this is very close to what I need however it does not return the elements not matching the predicate till the first match. I need all unmatching including the first matching elements. – metrik Oct 12 '22 at 10:49
  • You can create a mutable list and keep adding the items in the list inside the lambda. I don't understand what problem you are facing here. Could you please elaborate a bit? – Arpit Shukla Oct 12 '22 at 11:23
  • I think you are right and `first` would work the same if I call the functions in the predicate, as in your example. I don`t remember why I assumed that this wouldn`t work. I will take another look at the implementation tomorrow and get back to you. – metrik Oct 12 '22 at 17:51
1

You can launch a new coroutine inside repeatOnLifecycle function scope, because launch function returns a Job so you can use it to cancel your job later.

Moreover, if you want to listen multiple flows inside repeatOnLifecycle scope, you'll need to launch a child coroutine to let them run in parallel. So they won't block each other and canceling a flow will not affect other flows.

Example code:

lateinit var job: Job

viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        job = launch {
            flowOfInterest.collectLatest {
                if (it) {
                    stopCollection()
                    doOnTrue()
                } else {
                    doOnFalse()
                }
            }
        }
    }
}

func stopCollection() {
    job.cancel()
}
Anh Tran
  • 131
  • 1
  • 5
  • Maybe I am wrong, but in this case suspend functions called from `doOnTrue` (or doOnTrue itself) are also cancelled or not? – metrik Oct 12 '22 at 10:41
1

Answering my own question here. What I needed was something like a takeWhile operator that includes the first non-matching element. Such an opeator can be created using the transformWhile operator like this:

viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        flowOfInterest.transformWhile {
            emit(it)
            !it
        }.collectLatest {
            if (it) {
                doOnTrue()
            } else {
                doOnFalse()
            }
        }
    }
}

This is not as nice and compact as I had hoped, but it works.

Edit: Alternatively, you can use Arpit Shukla's answer and perform the actions in the predicate of the 'first' function.

metrik
  • 17
  • 1
  • 6
0

You can also use takeWhile to define condition which controls collecting from flow.

 flowOfInterest.takeWhile { !it }.collect {
     //execute smth
 }

If your flow is "cold" after reaching the state, when the condition is not met, the flow will stop emitting at all if there is no more observers. Otherwise, it will continue (e.g. if you use shared flow), but your current observer (which you applied to the question) will stop collecting.

As a workaround to your specific case, if your flow is not "cold", you can call

flowOfInterest.take(1).collect { ... } 

to receive the element, on which the predicate returns false.

You can also use additional boolean value that you will modify in collect block when you will reach some condition, which will be the predicate for takeWhile.

Steyrix
  • 2,796
  • 1
  • 9
  • 23
  • Thanks but this will not work for my use case since, according to the documentation, "it does *not* contain the element on which the predicate returned false". – metrik Oct 12 '22 at 10:39
  • @metrik I extended my answer, maybe it helps – Steyrix Oct 12 '22 at 11:33