7

I have a repository class with an asynchronous method returning User wrapped into a LiveData:

interface Repository {
    fun getUser(): LiveData<User>
}

In a ViewModel's coruotine scope I want to wait for a result of getUser() method and use an User instance.

this is what, I am looking for:

private fun process() = viewModelScope.launch {
   val user = repository.getUser().await()
   // do something with a user instance
}

I could not find LiveData<>.await() extension method, and any attempts to implement it. So before doing it my self, I wonder maybe there is some better way?

All solutions that I have found were about making getUser() a suspend method, but what if I can not change Repository?

nsk
  • 310
  • 2
  • 9

5 Answers5

10

You should be able to create an await() extension function using suspendCancellableCoroutine(). This probably is not exactly correct, but something along these lines should work:

public suspend fun <T> LiveData<T>.await(): T {
  return withContext(Dispatchers.Main.immediate) {
    suspendCancellableCoroutine { continuation ->
      val observer = object : Observer<T> {
        override fun onChanged(value: T) {
          removeObserver(this)
          continuation.resume(value)
        }
      }

      observeForever(observer)

      continuation.invokeOnCancellation {
        removeObserver(observer)
      }
    }
  }
}

This should return the first value emitted by the LiveData, without leaving an observer behind.

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • 1
    I think you might need to wrap this function in `withContext(Dispatchers.Main)`, since `observeForever` can only be called on the main thread. Unless, I suppose, you're following a convention that all suspend functions are safe to call only from the Main dispatcher. – Tenfour04 Jun 19 '20 at 17:19
  • 1
    @Tenfour04: Good point -- I have updated the example, though I used `Dispatchers.Main.immediate`. Thanks! – CommonsWare Jun 19 '20 at 17:23
  • 1
    I'd argue that it is safer to as `asFlow().first()`. `invokeOnCancellation` is called synchronously which is a little worrying. – Dominic Fischer Jun 19 '20 at 18:49
  • @DominicFischer: That's a simpler possibility, albeit with more "moving parts" under the covers. `asFlow()`, as currently constructed, uses a conflated `Channel` as an intermediary between the `LiveData` and the resulting `Flow`, and that worries me a bit -- you might not strictly get the first object returned from the `LiveData`. – CommonsWare Jun 19 '20 at 19:05
  • 1
    We can meet in the middle with a `CompletableDeferred`. – Dominic Fischer Jun 19 '20 at 20:51
  • @DominicFischer: That's an interesting option. Though the function name `completeExceptionally()` makes me giggle. :-) – CommonsWare Jun 19 '20 at 20:56
  • 2
    Since `continuation.invokeOnCancellation()` can be invoked on any thread, it's important to ensure we are on the main thread by posting a message on a Handler before calling `removeObserver(observer)` – BladeCoder May 25 '21 at 08:53
1
suspend inline fun <T> suspendCoroutineWithTimeout(
    timeout: Long,
    crossinline block: (CancellableContinuation<T>) -> Unit
): T? {
    var finalValue: T? = null
    withTimeoutOrNull(timeout) {
        finalValue = suspendCancellableCoroutine(block = block)
    }
    return finalValue
}

suspend inline fun <T> suspendCoroutineObserverWithTimeout(
    timeout: Long,
    data: LiveData<T>,
    crossinline block: (T) -> Boolean
): T? {
    return suspendCoroutineWithTimeout<T>(timeout) { suspend ->
        var observers : Observer<T>? = null
        val oldData = data.value
         observers = Observer<T> { t ->
             if (oldData == t) {
                 KLog.e("参数一样,直接return")
                 return@Observer
             }
             KLog.e("参数不一样,刷新一波")
            if (block(t) && !suspend.isCancelled) {
                suspend.resume(t)
                observers?.let { data.removeObserver(it) }
            }
        }

        data.observeForever(observers)
        suspend.invokeOnCancellation {
            KLog.e("删除observiers")
            observers.let { data.removeObserver(it) }
        }
    }
}
  • The previous @EpicPandaForce answers have been answered very well, I added a type judgment on this basis, and I will return only when the conditions are met. Sorry, my English is not very good. – 乐于助人的王老师 Jul 05 '21 at 08:47
  • Please don't post only code as answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes. – Tyler2P Jul 10 '21 at 13:31
  • This is a good suggestion, thanks @Tyler2P – 乐于助人的王老师 Jul 16 '21 at 05:49
0

here is an extension function that suits your needs, this function also include maximum wait time parameter.

fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    afterObserve.invoke()

    // Don't wait indefinitely if the LiveData is not set.
    if (!latch.await(time, timeUnit)) {
        this.removeObserver(observer)
        throw TimeoutException("LiveData value was never set.")
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}
ilatyphi95
  • 555
  • 5
  • 22
-1

True Kotlin way was to change Repository interface and make getUser() a suspend method.

nsk
  • 310
  • 2
  • 9
-1

You can actually "await" for live data inside coroutine

it is a workaround, there might be better options:

suspend fun retrieveUser(): LiveData<User> {
     val liveUser: MutableLiveData<User> = MutableLiveData()

     var counter = 0
     val timeOut = 20 // 2 sec

     while(liveUser.value.isNullOrEmpty()) {
          if(counter > timeout) break
          counter++
          delay(100)
     }

     return liveUser
Dan Dfg
  • 1
  • 2