Skip to content
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

Proposal: Flow.collectLatest #1269

Closed
BoxResin opened this issue Jun 12, 2019 · 13 comments
Closed

Proposal: Flow.collectLatest #1269

BoxResin opened this issue Jun 12, 2019 · 13 comments
Assignees
Labels

Comments

@BoxResin
Copy link

Function signature

suspend fun <T> Flow<T>.collectLatest(action: suspend (value: T) -> Unit): Unit

Behavior

val flow = flowOf(1, 2, 3, 4, 5)
flow.collectLatest { value: Int -> // This lambda would be canceled if a new value is emitted although it hasn't been finished.
    delay(100)
    println(value) // So only '5' will be printed.
}
@BoxResin
Copy link
Author

Possible Implementation I think:

suspend inline fun <T> Flow<T>.collectLatest(crossinline action: suspend (value: T) -> Unit) {
    var job: Job? = null
    val scope = CoroutineScope(coroutineContext)

    this.collect { value: T ->
        job?.cancelAndJoin()
        job = scope.launch { action(value) }
    }
    job?.join()
}

@LouisCAD
Copy link
Contributor

I have used such a thing for a long time (also have a version for channels), with a longer name, and the ability to skip equals:
https://github.com/LouisCAD/Splitties/blob/develop/samples/android-app/src/androidMain/kotlin/com/example/splitties/extensions/coroutines/Flow.kt#L18-L35

I need it a lot of cases, so I'd vote for it coming here.

@elizarov
Copy link
Contributor

What's the use-case? I get the example of usage, but where would you actually use it?

@BoxResin
Copy link
Author

BoxResin commented Jun 14, 2019

@LouisCAD
The ability to skip equals may be replaced by Flow.distinctUntilChanged().
In combination with it, you can do the same thing.

For example:

val flow = flowOf(1, 2, 2, 3).delayEach(100)
flow.distinctUntilChanged().collectLatest {
    // Do something
}

@elizarov elizarov added the flow label Jun 14, 2019
@LouisCAD
Copy link
Contributor

@BoxResin Yes, but it's a little less efficient, and may force an additional indentation level of the lambda.

@LouisCAD
Copy link
Contributor

@elizarov Here's an example: https://github.com/LouisCAD/Splitties/blob/develop/samples/android-app/src/androidMain/kotlin/com/example/splitties/main/MainActivity.kt#L68-L90

Can also be helpful if you want to have a loop running while in a certain state that is emitted from the flow.

@BoxResin
Copy link
Author

BoxResin commented Jun 14, 2019

@elizarov Here's another example:

object Config {
    val school: Flow<School?> = ...
}

class DailyMealViewModel(val date: TimePoint) : CoroutineScope {
    private val job = SupervisorJob()
    override val context = Dispatchers.Main + job

    val content = MutableLiveData<DailyMealViewContent>()

    init {
        // `this.launch { ... }` would be canceled when `this.onCleared()` is called.
        this.launch {
            Config.school.collectLatest { school: School? ->
                // This function will be canceled when a new school emitted.
                this.onSchoolChanged(school)
            }
        }
    }

    private suspend fun onSchoolChanged(school: School?) {
        if (school == null) {
            this.content.value = DailyMealViewContent.Error(
                message = "Please select school",
                button = "Select" to this::onClickSchoolSetting
            )
            return
        }

        MealDatabase.observe(school.code, this.date).collectLatest { dailyMeal: DailyMeal? ->
            // This function will be canceled when the `dailyMeal` in database changed.
            this.onDailyMealChanged(dailyMeal)
        }
    }

    private suspend fun onDailyMealChanged(dailyMeal: DailyMeal?) {
        if (dailyMeal == null) {
            this.content.value = DailyMealViewContent.Error(
                message = "Please download meal data",
                button = "Download" to this::onClickDownloadMeal
            )
            return
        }
        
        // Something to deal with `dailyMeal`
        ...
    }

    override fun onCleared() {
        this.job.cancel()
    }
}

@elizarov
Copy link
Contributor

We've recently added .conflate() operator and it seems that collectLatest { ... } is almost equivalent to conflate().collect { ... }. See the following example in playground: https://pl.kotl.in/JcBskQvFE

There:

    val flow = flowOf(1, 2, 3, 4, 5)
    flow.conflate().collect { value: Int -> 
        delay(100)
        println(value)
    }    

prints 1 and 5. It does not rely on the cancellation of the collecting lambda though. Still, might it cover some of the use-cases?

@elizarov
Copy link
Contributor

Another observise, the requested behavior for collectLatest seems very much alike to switchMap but for a suspending function call, as opposed to the switching on the flow...

@BoxResin
Copy link
Author

@elizarov Yes, I considered switchMap at first. Its behavior is very similar to collectLatest but the use-case looks weird.

suspend fun main() {
    val flow = flowOf(1, 2, 3, 4, 5)
    flow.switchMap { it -> flowOf(it) }.collect { value: Int -> 
        delay(100)
        println(value) // So only '5' will be printed.
    }    
}

I just want to take the 'switching' behavior of switchMap without 'mapping'.

@elizarov
Copy link
Contributor

Thanks. That is an interesting observation and it raises the whole set of issues with respect to naming. I wonder if we've making a mistaking of providing switchMap operator in the first place. Here I've recorded by thoughts: #1335

@BoxResin
Copy link
Author

@elizarov
I found a more common use-case.

val accountFlow: Flow<Account?> // null value means that user has signed out.

class Account {
    val emailFlow: Flow<String>
    val nicknameFlow: Flow<String>
    val phoneNumberFlow: Flow<String>
}

// Suppose this code is in a suspend lambda.
accountFlow.collectLatest { account: Account? ->
    if (account != null) {
        showAccountUI()

        // This lambda should be canceled when a new account is emitted.
        account.emailFlow.collect { email: String -> /* Redraw email UI */ }

        // This lambda should be canceled when a new account is emitted.
        account.nicknameFlow.collect { nickname: String -> /* Redraw nickname UI */ }

        // This lambda should be canceled when a new account is emitted.
        account.phoneNumberFlow.collect { phoneNumber: String -> /* Redraw phone number UI */ }
    } else {
        showEmptyAccountUI()
    }
}

The accountFlow may emit null value, so accountFlow.switchMap { it.emailFlow }.collect { ... } is not available in this case.

@qwwdfsad
Copy link
Collaborator

qwwdfsad commented Jul 25, 2019

@BoxResin please note that Flow (as opposed to Rx) supports nulls, so using switchMap is still possible:

suspend fun Flow<T>.collectLatest(action: suspend (value: T) -> Unit) = switchMap { flowOf(it) }.collect(action)

qwwdfsad added a commit that referenced this issue Jul 30, 2019
qwwdfsad added a commit that referenced this issue Aug 6, 2019
qwwdfsad added a commit that referenced this issue Aug 9, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants