Wait for LiveData result in a Kotlin coroutine

Viewed 5398

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?

5 Answers

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.

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
}
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) }
        }
    }
}

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

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

Related