-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Flow.takeUntil(otherFlow) #1850
Comments
What is your use-case for such an operator? Can you give an example on how you'd use it in your application? |
Sure. I need to cancel the flow. sealed class CatAction {
object StartFetch: CatAction()
object CancelFetch: CatAction()
data class UpdateCats(val cats: List<Cat>): CatAction()
}
suspend fun fetchCats(): List<Cat> {
return httpClient.fetchCats()
}
fun catFetcher(actions: Flow<CatAction>): Flow<CatAction> {
return actions
.mapNotNull { it as? CatAction.FetchCat }
.flatMapConcat { action ->
flow {
val cats = fetchCats()
emit(UpdateCats(cats))
}
.takeUntil(actions.mapNotNull { it as? CatAction.CancelFetch })
}
} |
This is a super-scary code, since you are collecting
Will it work for you? |
No. It's not work for me. I need to process only one So I need this behavior: UPD: sealed class CatAction {
object StartFetch: CatAction()
object CancelFetch: CatAction()
data class UpdateCats(val cats: List<Cat>): CatAction()
}
data class CatState(
val cats: List<Cat>? = null,
val isLoading: Boolean = false
)
suspend fun fetchCats(): List<Cat> {
return httpClient.fetchCats()
}
fun catFetcher(actions: Flow<CatAction>, state: StateProvider<CatState>): Flow<CatAction> {
return actions
.filterNot { state.isLoading }
.mapNotNull { it as? CatAction.FetchCat }
.flatMapConcat { action ->
flow {
val cats = fetchCats()
emit(UpdateCats(cats))
}
.takeUntil(actions.mapNotNull { it as? CatAction.CancelFetch })
}
} |
The example you've given still has the problem of collection You use-case can be implemented with some kind of
|
Yeah. It can be implemented with I think fun catFetcher(actions: Flow<CatAction>): Flow<CatAction> {
return actions
.dropWhenBusy { it is CatAction.FetchCat } // ignore featch requests when working on prev
.filter { it is CatAction.FetchCat || it is CatAction.CancelFetch } // ignore actions
.transformLatest { action ->
// this block will get cancelled as soon as cancel action comes in
if (action is CatAction.FetchCat) emit(UpdateCats(fetchCats()))
}
} |
@rougsig What alternative would you suggest? |
An operator that can cancel Flow on an event from another Flow. In basic some rx conditional operators. |
@rougsig Do you have a use-case for cancelling a flow on an event from another flow? The use-case we've been discussing so far was about cancelling a flow on an event from the same flow. |
Sure: sealed class LifeCycleAction {
object Create: LifeCycleAction()
object Destroy: LifeCycleAction()
}
sealed class CatAction {
object StartFetch: CatAction()
data class UpdateCats(val cats: List<Cat>): CatAction()
}
val lifeCycle: Channel<LifeCycleAction>
val requests: Channel<CatAction>
val cats: Flow<List<Cat>>
// all computation will be calculated on other context.
// in that reason we can not use job.cancel()
fun catFetcher(actions: Flow<CatAction>, lifeCycle: Flow<LifeCycleAction>): Flow<CatAction> {
return actions
.filterNot { state.isLoading }
.mapNotNull { it as? CatAction.StartFetch }
.flatMapConcat { action ->
flow {
val cats = fetchCats()
emit(UpdateCats(cats))
}
.takeUntil(actions.mapNotNull { it as? CatAction.CancelFetch })
}
}
class Screen: CoroutineScope {
fun onCreate() {
launch {
lifeCycle.send(LifeCycleAction.Create)
requests.send(CatAction.StartFetch)
}
launch {
cats.collect { cats -> renderCats(cats) }
}
}
fun onDestroy() {
launch { lifeCycle.send(LifeCycleAction.Destroy) }
}
} But in my pet app i have one Flow with all application fun catFetcher(actions: Flow<CatAction>): Flow<CatAction> {
return actions
.dropIf { it is CatAction.CancelFetch }
.mapNotNull { it as? CatAction.FetchCat }
.map { action ->
emit(UpdateCats(fetchCats()))
}
} |
@rougsig I'm a bit lost. You're showing the same code that, in essence, does:
It does not work this way. You can get really weird results. Do you actually have a use-case where it comes from different flows? |
No. In real it only one Flow. |
Then you need some other solution to your problem that would not use |
Tomorrow I will add comment with link to the simple Android application for this case. |
I creating pet project with flow being inspired by https://redux-observable.js.org/ import { ajax } from 'rxjs/ajax';
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action => ajax.getJSON(`/api/users/${action.payload}`).pipe(
map(response => fetchUserFulfilled(response)),
takeUntil(action$.pipe(
ofType(FETCH_USER_CANCELLED)
))
))
); Why can I get really strange results? in this case: actions .... /* some transformation */
.takeUntil(actions.mapNotNull { it as? CatAction.CancelFetch })
// ^^^^^^^^ uses _the same_ "actions" flow here |
So all the cases I had can be solved using transformLatest and combining the start and cancel events into one. Thank you. |
This would be a valid use-case in my opinion for val cancelChannel: BroadCastChannel<Unit>
val intervalFlow: Flow<Unit> // emits every x TimeUnit
intervalFlow
.takeUntil(cancelChannel.asFlow())
.onEach { /* do something */ }
.launchIn(someScope)
// later
cancelChannel.offer(Unit) // after this, intervalFlow should no longer emit |
@floschu Where you might encounter a use-case like this? You can use structured concurrency for this kind of cancellation. Can you elaborate on your example, please. |
|
If the intervalFlow is not launched by me, I would not know how to cancel it via structured concurrency, maybe you could elaborate on this. library: package com.library
abstract class Store<Action, Mutation, State> {
protected val actions = BroadCastChannel<Action>(BUFFERED)
open var mutator: (action: Action) -> Flow<Mutation> = { emptyFlow() }
init {
actions.asFlow()
.flatMapMerge { action -> mutator(action) }
.scan(initialState) { previousState, mutation -> reduce(previousState, mutation) }
.onEach { /* publish state */ }
.launchIn(someScope)
}
} implementation: package com.mine
enum class Action { LOAD }
class MyStore : Store<Action, Int, Int> {
override var mutator = { action ->
when(action) {
Action.LOAD -> flow {
val value: Int = someSuspendingOperation()
emit(value)
}.takeUntil(actions.asFlow().filter { it is Action.LOAD })
}
}
} In this case I want the created |
@floschu Your latest example is not reliable. You are, again, collecting the same How to write it correctly? I don't have a ready answer. I'll need to know more about this kind of architecture to find a solution. Would |
I do not see how an example with multiple actions would make you understand the architecture better 😅 I have an elaborate example for you, but it would be a bit of a bigger read I guess: In general I need a way to cancel the In RxJava I would have used the |
@elizarov I this operator is most useful when a flow is used to represent a UI event.
this is a common use-case in RXjava (see rxbindings) and it is already been replicated with co-routines (see flow bindings) |
@RoryKelly The example you've posted is relatively safe, since it actually combines three different flows ( |
@elizarov I think the threading issues caused by
The coroutine would throw when the In other stream implementations, the Ux problem you outlined I think |
My use case is I'd like to collect flow A only while some condition is true represented by flow B. A A real world example is collecting location data only while the user has granted permission to do so. |
I need this operator. It is useful for cancel running request when clicking a button fetchFlow.flatMapLatest { request().takeUntil(cancelFlow) } |
I have added it in https://github.com/hoc081098/FlowExt libraryMarble
@ExperimentalCoroutinesApi
fun <T, R> Flow<T>.takeUntil(notifier: Flow<R>): Flow<T> = channelFlow {
val outerScope = this
launch {
try {
notifier.take(1).collect()
close()
} catch (e: CancellationException) {
outerScope.cancel(e) // cancel outer scope on cancellation exception, too
}
}
launch {
try {
collect { send(it) }
close()
} catch (e: CancellationException) {
outerScope.cancel(e) // cancel outer scope on cancellation exception, too
}
}
} |
@hoc081098 thank you so much! I was trying to do this yesterday and I didn't get it right. I think that your snippet is missing one |
That's right. Thanks @BraisGabin |
I need analog for rxJava takeUntil operator. This logic can be found here
Will such an operator be added to the standard library?
The text was updated successfully, but these errors were encountered: