Unsubscribe from Kotlin Flow without cancelling current scope - android

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!

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
}
}
}

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.

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()
}

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.

Related

How can I get data from ViewModel to Activity or Fragment in clean and simple way?

I have a question... sometimes, I need to get data from ViewModel directly. For example, Let's say there's a isChecked() method in ViewModel. And I want to use it in the if condition.
if(viewModel.isChecked()){
// TODO:
}
So, what I am doing right now is:
fun isChecked(): Boolean = runBlocking {
val result = dbRepo.getData()
val response = apiRepo.check(result)
return response.isSuccessful
}
It uses runBlocking. So, it runs on MainThread. I don't think it's a good way because it can freeze the screen. But yes, if the condition needs to run, it needs to wait it until it gets the data from DB and Network.
Another way that I can think of is using LiveData. However, I can't use it in the condition. So, I needs to move the condition in the observer block. But sometimes, this can't be done because there can be something before the condition. And it doesn't seem to look direct but writing code here and there and finally get that data.
So, Is there any simpler way than this?
Your best bet if you have something slow or blocking like that is to rethink how you are using the data entirely. Instead of trying to return it, use LiveData or callbacks to handle the response asynchronously without causing your UI to hang or become laggy. In these cases you really only have three options:
Use a callback to handle when the response is received
Use observable data like LiveData to handle when the response is received
Change the method to a suspend function and call it from a coroutine
Forcing a method to wait to return on the main thread without using one of these is going to cause the app to hang.
Callback to get state
It's hard to say definitely what the best solution for you is without more details about how you are using isChecked(), but one pattern that could work would be to use a callback to handle what you were formerly putting in the if statement, like this (in the ViewModel):
fun getCheckedState(callback: (Boolean)->Unit) {
viewModelScope.launch {
// do long-running task to get checked state,
// using an appropriate dispatcher if needed
val result = dbRepo.getData()
val response = apiRepo.check(result)
// pass "response.isSuccessful" to the callback, to be
// used as "isChecked" below
callback(response.isSuccessful)
}
}
You would call that from the activity or fragment like this:
viewModel.getCheckedState { isChecked ->
if( isChecked ) {
// do something
}
else {
// do something else
}
}
// CAUTION: Do NOT try to use variables you set inside
// the callback out here!
A word of caution - the code inside the callback you pass to getCheckedState does not run right away. Do not try to use things you set inside there outside the callback scope or you fall into this common issue
Simpler Callback
Alternately, if you only want to run some code when isChecked is true, you could simplify the callback like this
fun runIfChecked(callback: ()->Unit) {
viewModelScope.launch {
// do long-running task to get checked state,
// using an appropriate dispatcher if needed
val result = dbRepo.getData()
val response = apiRepo.check(result)
// only call the callback when it's true
if( response.isSuccessful ) {
callback()
}
}
}
and call it with
viewModel.runIfChecked {
// do something
}
// Again, don't try to use things from the callback out here!
Use lifecyclescope.launch(Dispatcher.IO) instead of runblocking
Try this code on your ViewModel class:
suspend fun isChecked(): Boolean {
val response: Response? = null
viewModelScope.launch(Dispatchers.IO) {
val result = dbRepo.getData()
response = apiRepo.check(result)
}.join()
return response?.isSuccessful
}
From Activity:
// Suppose you have a button
findViewById<Button>(R.id.btn).setOnClickListener({
CoroutineScope(Dispatchers.Main).launch {
if (viewModel.isChecked()) {
Log.d("CT", "Do your others staff")
}
}
})
Hope it work file. If no let me comment

Kotlin Flow not collected anymore after working initially

Basically I want to make a network request when initiated by the user, collect the Flow returned by the repository and run some code depending on the result. My current setup looks like this:
Viewmodel
private val _requestResult = MutableSharedFlow<Result<Data>>()
val requestResult = _requestResult.filterNotNull().shareIn(
scope = viewModelScope,
started = SharingStarted.WhileViewSubscribed,
replay = 0
)
fun makeRequest() {
viewModelScope.launch {
repository.makeRequest().collect { _requestResult.emit(it) }
}
}
Fragment
buttonLayout.listener = object : BottomButtonLayout.Listener {
override fun onButtonClick() {
viewModel.makeRequest()
}
}
lifecycleScope.launchWhenCreated {
viewModel.requestResult.collect { result ->
when (result) {
Result.Loading -> {
doStuff()
}
is Result.Success -> {
doDifferentStuff(result.data)
}
is Result.Failure -> {
handleError()
}
}
}
}
The first time the request is made everything seems to work. But starting with the second time the collect block in the fragment does not run anymore. The request is still made, the repository returns the flow as expected, the collect block in the viewmodel runs and emit() also seems to be executed successfully.
So what could be the problem here? Something about the coroutine scopes? Admittedly I lack any sort of deeper understanding of the matter at hand.
Also is there a more efficient way of accomplishing what I'm attempting using Kotlin Flows in general? Collecting a flow and then emitting the same flow again seems a bit counterintuitive.
Thanks in advance:)
According to the documentation there are two recommended alternatives:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
//your thing
}
}
I rather the other alternative:
viewLifecycleOwner.lifecycleScope.launch {
viewModel.makeReques().flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect {
// Process the value.
}
}
I like the flowWithLifecycle shorter syntax and less boiler plate. Be carefull thar is bloking so you cant have anything after that.
The oficial docs
https://developer.android.com/topic/libraries/architecture/coroutines
Please be aware you need the lifecycle aware library.

Cancelling the collect of the Kotlin flow

I have a parent class that has different states, this parent class has a list of child classes that have different states each. I want to collect on each one of them and cancel the one that reaches the Terminated state. Something like that:
coroutineScope.launch(Dispatcher.IO) {
parent.parentState.collect {
if(it is ParentState.Normal){
it.children.forEach{ child ->
coroutineScope.launch(Dispatcher.IO){
child.childState.collect{
if(it is ChildState.Terminated){
//when this line executed all the collectors stop until I change the states for each one of them..
this.coroutineContext.job.cancel()
} else{
// Do something else for any other state...
}
}
}
}
}
}
}
But when I do that all the children that I am collecting from stop collecting, but it starts collecting again If I changed the state for each one of them, which is wasn't the case before cancelling one of them.
So my question is why it behaves like that when cancelling the job for one of the collectors?
Also is there a better way "reactive way" to write this?
I totally agree that you can use SupervisorJob to deal with your problems.
But in my opinion, not so many sub-jobs are needed. A sub-job can solve the problems you encounter. In your code, a child coroutine of the size of children Collection will be created. Although the coroutine is very lightweight, I think it is unnecessary overhead.
You can completely convert List<Flow<T>> into Flow<List<T>> before each Flow collect. After that, only the converted single Flow can be collect.
Here is how I handled it:
inline fun <reified T> List<Flow<T>>.flattenFlow(): Flow<List<T>> = combine(this#flattenFlow) {
it.toList()
}
coroutineScope.launch(Dispatcher.IO) {
parent.parentState.collect {
if (it is ParentState.Normal) {
it.flattenFlow().collect {childStateList ->
childStateList.onEach {childState ->
if (childState is ChildState.Terminated) {
// Do something when state in Terminated..
} else {
// Do something else for any other state...
}
}
}
}
}
}
By default, coroutine scope is using Job() in CoroutineContext, Job() will cancel coroutine execution or any running child on canceled or exception thrown.
To keep other child execution remain active, you can use special Job, which is SupervisorJob()
CoroutineScope(SupervisorJob() + Dispatchers.IO)
Also, let's create a brand new scope for each child
val childScope = CoroutineScope(Dispatchers.IO)
childScope.launch {
child.childState.collect {
....
}
}
and you should cancel those childScope instead the coroutineContext.
Therefore, your code will looks like below
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scope.launch {
parent.parentState.collect {
if(it is ParentState.Normal){
it.children.forEach{ child ->
val childScope = CoroutineScope(Dispatchers.IO)
childScope.launch(Dispatcher.IO){
child.childState.collect{
if(it is ChildState.Terminated){
//when this line executed all the collectors stop until I change the states for each one of them..
childScope.cancel()
} else{
// Do something else for any other state...
}
}
}
}
}
}
}

How to pause/stop collecting/emitting data in a Flow while app minimised?

I have a UseCase and remote repository that return Flow in a loop and I collect the result of UseCase in the ViewModel like this:
viewModelScope.launch {
useCase.updatePeriodically().collect { result ->
when (result.status) {
Result.Status.ERROR -> {
errorModel.value = result.errorModel
}
Result.Status.SUCCESS -> {
items.value = result.data
}
Result.Status.LOADING -> {
loading.value = true
}
}
}
}
the problem is when the app is in the background (minimized) flow continues working. so can I pause it when the app is in the background and resume it when the app comes back to the foreground?
and also I don't want to observe the data in my view (fragment or activity).
I'd play around with the stateIn operator and the way I'm currently consuming the flow in the view.
Something like:
val state = useCase.updatePeriodically().map { ... }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed, initialValue)
And consume it from the View like:
viewModel.flowWithLifecycle(this, Lifecycle.State.STARTED)
.onEach {
}
.launchIn(lifecycleScope)
For other potential ways on how to collect flows from the UI: https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
EDIT:
If you don't want to consume it from the view, you still have to signal for the VM that your View is in the background currently.
Something like:
private var job: Job? = null
fun start(){
job = viewModelScope.launch {
state.collect { ... }
}
}
fun stop(){
job?.cancel()
}
Even if the viewModelScope is cancelled, the flow will continue to collect because it is not cooperative to cancellation.
To make a flow cancellable, you can do one of the following things:
In the collect lambda, call currentCoroutineContext().ensureActive() to make sure the context in which the flow is being collected is still active. This will however throw a CancellableException, which you will need to catch, if the coroutine scope was cancelled already (viewModel scope for your case.)
You can use cancellable() operator as follows:
myFlow.cancellable().collect { //do stuff here.. }
And you can call cancel() whenever you want to cancel the flow.
For official documentation on cancelling the flow see:
https://kotlinlang.org/docs/flow.html#flow-cancellation-checks
I believe you want something like this
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
state.collect {
}
}
}
Here's an execellent article on repeatOnLifecyle: https://medium.com/androiddevelopers/repeatonlifecycle-api-design-story-8670d1a7d333

Emit Flow values when the block is inside Runnable body

So I have this function where I can emit values with flow but I need to send values periodically and so I used:
fun hereIsAFunction(): Flow<Type> = flow {
Handler.postDelayed({
//This is in Runnable and I can't emit values
emit(value) //Error 'Suspension function can only be called within Coroutine body
}, 1000)
usingOtherFunction()
}
I don't want to block the function 'usingOtherFunction()', that's why I'm using a runnable
Question: Is there any way to emit values with Flow with periodically events? If yes, what should I look into?
According to your comment that you want to do something in parallel, you can launch 2 coroutines in a scope. They will run independent to each other:
fun hereIsAFunction(): Flow<Type> = flow {
coroutineScope {
launch {
(0..100).forEach {
delay(1000)
emit(...)
}
}
launch {
usingOtherFunction()
}
}
}
Flows are built on top of coroutines. You can use the delay(time) method of coroutines to delay the execution.
More details here: https://developer.android.com/kotlin/flow#create

Categories

Resources