Come chiamare la richiesta asincrona in modo sincrono con un valore di timeout?

Jan 07 2021

Devo chiamare in modo sincrono una richiesta API asincrona. Poiché questa richiesta API richiede molto tempo per rispondere, desidero anche impostare un timeout per non riuscire la richiesta API e continuare con null. Ecco il mio codice per chiamare questa API:

private suspend fun call(
    accessStage: AccessStage,
): Response? = withContext<Response?>(Dispatchers.IO) {

    return@withContext withTimeoutOrNull(1000) {

        suspendCoroutine<Response?> { continuation ->

            val request = External3rdPartyApi.newRequest(
                accessStage
            ) { response, throwable ->

                continuation.resume(response)
            }

            request.parameters = hashMapOf<String, String>().apply {
                put["token"] = External3rdPartyApi.TOKEN
                put["salt"] = External3rdPartyApi.calculateSalt(accessStage)
            }
            request.executeAsync()
        }
    }
}

Non posso cambiare il modo in cui External3rdPartyApifunziona.

Penso che il codice sopra sembri malvagio. Inoltre, ho letto in un'altra risposta :

withTimeout { ... }è progettato per annullare l'operazione in corso per timeout, che è possibile solo se l'operazione in questione è annullabile .

Quindi, dovrei usare al suspendCancellableCoroutineposto di suspendCoroutine?

Come posso scriverlo in un modo migliore?

Risposte

1 ChristianB Jan 07 2021 at 19:42

L'uso suspendCoroutineva bene se non puoi (o non vuoi) gestire la cancellazione della Coroutine. Ma poiché hai un timeout, dovresti considerare l'utilizzo di suspendCancellableCoroutine e gestire l'evento di annullamento per interrompere il lavoro (nella funzione di terze parti, se puoi).

suspendCancellableCoroutine<T> { continuation ->
  continuation.invokeOnCancellation { throwable ->
    // now you could stop your (third party) work  
  }        
}

Quando la tua funzione di terze parti genera un'eccezione, puoi provare a catturarla e terminare la continuazione utilizzando la ripresa con exceptiono restituire un nullvalore (dipende dal tuo caso d'uso):

suspendCancellableCoroutine<T?> { continuation ->
  try {
    continuation.resume(thirdParty.call())
  } catch (e: Exception) {
    // resume with exception
    continuation.resumeWithException(e)
    // or just return null and swallow the exception
    continuation.resume(null)
  }   
}

Mettiamo tutto insieme

suspend fun call(): Response? = withContext(Dispatchers.IO) {
  return@withContext withTimeoutOrNull(1000L) {
    return@withTimeoutOrNull suspendCancellableCoroutine { continuation ->
      try {
        continuation.resume(External3rdPartyApi.newRequest(accessStage))
      } catch (e: Exception) {
        // resume with exception
        continuation.resumeWithException(e)
        
        // or just return null and swallow the exception
        continuation.resume(null)
      }
         
      // in case the coroutine gets cancelled - because of timeout or something else
      continuation.invokeOnCancellation {
        // stop all the work
      }
    }
  }
}