nsk
nsk

Reputation: 330

Wait for LiveData result in a Kotlin coroutine

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?

Upvotes: 7

Views: 8340

Answers (5)

Dan Dfg
Dan Dfg

Reputation: 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

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

Upvotes: 1

CommonsWare
CommonsWare

Reputation: 1006594

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.

Upvotes: 10

nsk
nsk

Reputation: 330

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

Upvotes: -1

ilatyphi95
ilatyphi95

Reputation: 605

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()
            [email protected](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
}

Upvotes: 0

Related Questions