Skip to content

Flow firstOrNull support #1796

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

Closed

Conversation

bradynpoulsen
Copy link
Contributor

@bradynpoulsen bradynpoulsen commented Feb 11, 2020

Adds implementations for Flow:

  • suspend fun firstOrNull(): T?
  • suspend fun firstOrNull(predicate: suspend (T) -> Boolean): T?

It is not clear why they were intentionally left out in the original proposal (#1078) but introduced with single/singleOrNull

@bradynpoulsen bradynpoulsen changed the base branch from master to develop February 11, 2020 04:18
@bradynpoulsen bradynpoulsen force-pushed the feature/flow-firstOrNull branch from 6b179a1 to b8647a7 Compare February 11, 2020 04:19
@bradynpoulsen bradynpoulsen force-pushed the feature/flow-firstOrNull branch from b8647a7 to fd2ef83 Compare February 11, 2020 04:19
@fvasco fvasco mentioned this pull request Feb 11, 2020
@elizarov
Copy link
Contributor

What's your use-case for this function? Where does it show up?

@dave08
Copy link

dave08 commented Feb 12, 2020

My use case is I have a Flow that could emit 0 or 1 elements matching a certain criteria, singleOrNull { } will crash if there's 0 and first { } too.

@elizarov
Copy link
Contributor

@dave08 I'm not sure I understand. Could you just use .filter { ... }.singleOrNull() ? Are you really looking for singleOrNull { ... } (a variant with prediacate, just like available for standard collections) to cover your use-case?

@dave08
Copy link

dave08 commented Feb 12, 2020

That throws if there's more than one element that matches filter { } I need only the first one if there is, and if not, just null.

@LouisCAD
Copy link
Contributor

@elizarov firstOrNull returns null only if the flow completes and is empty. Otherwise, it returns the first value, and cancels the upstream flow. I currently see no combination of operators that can conveniently reproduce the behavior or firstOrNull.

@elizarov
Copy link
Contributor

elizarov commented Feb 12, 2020

@LouisCAD Indeed, that difference between firstOrNull and singleOrNull is that the former cancels the flow after it receives the first element, while the latter receives more at throws an exception if there's more than one. And that's why I'm puzzled. @dave08 said that this PR addresses a use-case of a flow that is known to have 0 or 1 elements. So why this PR is adding firstOrNull instead of singleOrNull?

@fvasco
Copy link
Contributor

fvasco commented Feb 12, 2020

It isn't too sexy, but it works.

suspend fun <T> Flow<T>.firstOrNull() = take(1).fold(null as T?) { _, v -> v }

A generic version

suspend fun <T> Flow<T>.getOrNull(index: Int) = take(index + 1).fold(null as T?) { _, v -> v }
suspend fun <T> Flow<T>.firstOrNull() = getOrNull(0)

@LouisCAD
Copy link
Contributor

@elizarov I understand why you're puzzled now. I think singleOrNull is a different use case, with semantics that are more complex because of implications: "single" implies assertion, but "null" implies the contrary. That's not straightforward if you add a predicate, it's no longer just 0 or 1 value or throw.

I think it is out of the scope of this PR that is for firstOrNull, which has simpler semantics (accepts any flow, never throws by itself, no kind of contradiction in the wording).

If singleOrNull { ... } is needed, people should probably open an issue about their use case first before possibly adding it if it makes sense.

@elizarov
Copy link
Contributor

I'm not just puzzled. I'm looking for use-cases. This PR proposes to add firstOrNull, but I don't have a use-case for it on the table. All I have for now is a use-case for singleOrNull.

@LouisCAD
Copy link
Contributor

@bradynpoulsen Can you give yours?

@RBusarow
Copy link
Contributor

RBusarow commented Feb 12, 2020

I've been using a no-arg version of firstOrNull for a while now.

With Android Bluetooth, we can only set the callback once (upon connection), so all responses are returned as a stream (Flow) and we need to parse them out. Hence our use for firstOrNull().

We're writing some data over Bluetooth (discover services, write a descriptor, write a characteristic), then observing a Flow for the response. Packets can be dropped or something can happen on the peripheral's end, so there's no guarantee of actually getting that response.

In send, we suspend until we get a value or the coroutine is cancelled upstream. In the case of writing to the Characteristic, it's wrapped in a withTimeoutOrNull.

interface GattFlows : BluetoothConnectionElement {
  ...
  fun characteristicWrite(uuid: UUID): Flow<CharacteristicWrite>
  ...
}
suspend fun sendCharacteristic(
  characteristic: Characteristic,
  data: ByteArray
) = withTimeoutOrNull(TIMEOUT) {

  send(gattFlows.characteristicWrite(characteristic.uuid)) {
    characteristic.value = data
    gatt?.writeCharacteristic(characteristic)
  }
}

internal suspend inline fun <E> send(
  flow: Flow<GattEvent<E>>, crossinline writeBlock: () -> Unit = {}
): E? = withContext(executor) {

  flow.onStart { writeBlock() }
    .firstOrNull()                             // < -- Here's the use-case
    ?.let { response ->
      if (response.isSuccessful) {
        response.value
      } else null
    }
}

Coincidentally, it's the same function as the use-case as for #1758, but for a different reason.

@dave08
Copy link

dave08 commented Feb 12, 2020

My original use case was to listen for the first finished WorkInfo in Android WorkManager's getWorkInfosByTagLiveData().asFlow(), I don't want it to crash if by mistake there's another one following the first that is also finished, but I don't know if it will for some reason complete the flow without any finished results, I just don't need the extra check whether it's single, but I also don't want it to crash on none like first { }.

I guess my case is really just for prevention of unnecessary crashing (which might not really be a concern, but why take that chance?) when not knowing a third party api well enough... I could technically use a try/catch for that though it might be less clear that I'm only really interested in 0 or 1 emissions (even though there could be more) that match the criteria.

@LouisCAD
Copy link
Contributor

@dave08 Using first is safe in your case as the LiveData never ends, making the Flow never complete.

@qwwdfsad qwwdfsad force-pushed the develop branch 3 times, most recently from 4a49830 to aff8202 Compare March 10, 2020 17:27
@qwwdfsad
Copy link
Collaborator

Superseded by #1869

@qwwdfsad qwwdfsad closed this Mar 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants