23

How can I get the value of a Flow outside a coroutine similarly to LiveData?

// Suspend function 'first' should be called only from a coroutine or another suspend function
flowOf(1).first()
// value is null
flowOf(1).asLiveData().value
// works
MutableLiveData(1).value

Context

I'm avoiding LiveData in the repository layer in favor of Flow. Yet, I need to set, observe and collect the value for immediate consumption. The later is useful for authentication purpose in a OkHttp3 Interceptor.

maxbeaudoin
  • 6,546
  • 5
  • 38
  • 53
  • 3
    `asLiveData` creates a LiveData that starts its own coroutine to collect the values from the Flow. The initial value of `value` in your example above would be `null` because there hasn't been a chance for the coroutine to start yet. – Tenfour04 Apr 13 '20 at 20:59
  • 2
    The point of Flow is to use it for getting data that is too time-consuming to do synchronously, so you would never want to get a value from it without a coroutine. – Tenfour04 Apr 13 '20 at 21:03
  • @Tenfour04 it is null indeed. I'm not exposing LiveData from the repository layer as pointed out by CommonsWare in many answers. Yet I need observability. – maxbeaudoin Apr 13 '20 at 21:12
  • 2
    There is no "value of a Flow". `LiveData` is a value holder, so it has a value (or `null`) at any point in time. `Flow` is just a stream. A `BroadcastChannel` is the coroutines equivalent of a value holder with observability, though that's still labeled as an experimental API IIRC. – CommonsWare Apr 13 '20 at 21:28
  • @CommonsWare thank you for your comment. Would you say it is acceptable in this case to expose a LiveData object in the repository? – maxbeaudoin Apr 13 '20 at 21:32
  • 1
    If your interceptor can deal with `null`, you can use `LiveData` or `BroadcastChannel` or just having the repository keep a cache of the last-received value and return it via a separate function. If your interceptor cannot deal with `null`, then you need to add a separate blocking API to the repository that your interceptor can use, or try consuming the `Flow` using `first()` inside `runBlocking()` to force synchronous behavior. – CommonsWare Apr 13 '20 at 21:38
  • OK, I adapted my comments into an answer. While writing it, it dawned on me that my suggestion of `Flow` with `runBlocking()` may or may not be practical, depending on what this `Flow` is, how it gets its data, etc. So I can't give you a clean out-of-the-box solution for the scenario where your interceptor must have the data. – CommonsWare Apr 13 '20 at 22:10

5 Answers5

34

You can do this

val flowValue: SomeType
runBlocking(Dispatchers.IO) {
    flowValue = myFlow.first()
}

Yes its not exactly what Flow was made for.

But its not always possible to make everything asynchronous and for that matter it may not even always be possible to 'just make a synchronous method'. For instance the current Datastore releases (that are supposed to replace shared preferences on Android) do only expose Flow and nothing else. Which means that you will very easiely get into such a situation, given that none of the Lifecycle methods of Activities or Fragments are coroutines.

If you can help it you should always call coroutines from suspend functions and avoid making runBlocking calls. A lot of the time it works like this. But it´s not a surefire way that works all the time. You can introduce deadlocks with runBlocking.

Max
  • 1,536
  • 1
  • 14
  • 18
  • Thanks for the warnings. To add on this, with `runBlocking` you are likely to block the UI Main thread, which should be avoided if at all possible. – sulai Mar 04 '22 at 09:16
  • I precisely got stuck in a datastore preferences situation I needed a value and it's in a flow. – Udayaditya Barua Dec 29 '22 at 11:43
12

Well... what you're looking for isn't really what Flow is for. Flow is just a stream. It is not a value holder, so there is nothing for you retrieve.

So, there are two major avenues to go down, depending on what your interceptor needs.

Perhaps your interceptor can live without the data from the repository. IOW, you'll use the data if it exists, but otherwise the interceptor can continue along. In that case, you can have your repository emit a stream but also maintain a "current value" cache that your interceptor can use. That could be via:

  • BroadcastChannel
  • LiveData
  • a simple property in the repository that you update internally and expose as a val

If your interceptor needs the data, though, then none of those will work directly, because they will all result in the interceptor getting null if the data is not yet ready. What you would need is a call that can block, but perhaps evaluates quickly if the data is ready via some form of cache. The details of that will vary a lot based on the implementation of the repository and what is supplying the Flow in the first place.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • 3
    Room is supplying the `Flow` in the first place. I use Room as a persistent storage for the current user and token. – maxbeaudoin Apr 13 '20 at 22:14
  • @maxbeaudoin: OK, then, the crudest-possible implementation would be to add a separate blocking DAO function that your repository in turn exposes, that the interceptor uses. – CommonsWare Apr 13 '20 at 22:20
  • Thank you. I have much to consider. – maxbeaudoin Apr 13 '20 at 22:22
  • @CommonsWare, Won't making a new dao (query call to DB) more resource extensive than getting from already available Flow>? – Cyber Avater Sep 01 '22 at 15:50
  • @CyberAvater: A `Flow` isn't "already available", as a `Flow` does not hold anything. It is a stream, nothing more. It is not a value holder, so there is nothing for you retrieve. Certain *types* of flows, such as a `StateFlow`, hold state, but that's not part of the question. – CommonsWare Sep 01 '22 at 15:59
2

You could use something like this:

    fun <T> SharedFlow<T>.getValueBlockedOrNull(): T? {
        var value: T?
        runBlocking(Dispatchers.Default) {
            value = when (this@getValueBlockedOrNull.replayCache.isEmpty()) {
                true -> null
                else -> this@getValueBlockedOrNull.firstOrNull()
            }
        }
        return value
    }
braintrapp
  • 748
  • 9
  • 11
2

To get the value of a Flow outside of a coroutine, the best option is to create the flow as a StateFlow and then call the value property on the StateFlow.

class MyClass {
   private val mutableProperty = MutableStateFlow(1)
   val property = mutableProperty.asStateFlow()

   ...
   mutableProperty.value = 2
}
...

val readProperty = MyClass().property.value
val propertyAsFlow = MyClass().property as Flow<Int>
Phileo99
  • 5,581
  • 2
  • 46
  • 54
1

You can use MutableStateFlow and MutableSharedFlow for emitting the data from coroutine and receiving the data inside Activity/Fragment. MutableStateFlow can be used for state management. It requires default value when initialised. Whereas MutableSharedFlow does not need any default value.

But, if you don't want to receive stream of data, (i.e) your API call sends data only once, you can use suspend function inside coroutine scope and the function will perform the task and return the result like synchronous function call.

Nandha Kumar
  • 131
  • 3
  • 13