/kotlin-coroutines-retrofit

Kotlin Coroutines await() extension for Retrofit Call

Primary LanguageKotlinApache License 2.0Apache-2.0

Kotlin Coroutines for Retrofit

CircleCI codecov codebeat badge

This is a small library that provides the Kotlin Coroutines suspending extension Call.await() for Retrofit 2

Based on kotlinx.coroutines implementation

Download

Download the JAR:

Gradle:

compile 'ru.gildor.coroutines:kotlin-coroutines-retrofit:0.11.0'

Maven:

<dependency>
  <groupId>ru.gildor.coroutines</groupId>
  <artifactId>kotlin-coroutines-retrofit</artifactId>
  <version>0.11.0</version>
</dependency>

How to use

NOTE: All examples in this README use runBlocking to build coroutine but it is only useful for testing or examples.

For a real application you probably want to use some other coroutines builder that does not block a thread, for example launch from kotlinx.coroutines.

If you want to use this library for UI please also check the Guide to UI programming with coroutines

There are three suspending extensions:

.await()

Common await API that returns a result or throws an exception

fun Call<T>.await(): T

In case of an HTTP error or an invocation exception await() throws an exception

// You can use retrofit suspended extension inside any coroutine block
fun main(args: Array<String>) = runBlocking {
    try {
        // Wait (suspend) for result
        val user: User = api.getUser("username").await()
        // Now we can work with result object
        println("User ${user.name} loaded")
    } catch (e: HttpException) {
        // Catch http errors
        println("exception${e.code()}", e)
    } catch (e: Throwable) {
        // All other exceptions (non-http)
        println("Something broken", e)
    }
}

.awaitResponse()

Common await API that returns a Response or throws an exception

fun Call<T>.awaitResponse(): Response<T>

In case of an invocation exception awaitResponse() throws an exception

// You can use retrofit suspended extension inside any coroutine block
fun main(args: Array<String>) = runBlocking {
    try {
        // Wait (suspend) for response
        val response: Response<User> = api.getUser("username").awaitResponse()
        if (response.isSuccessful()) {
          // Now we can work with response object
          println("User ${response.body().name} loaded")
        }
    } catch (e: Throwable) {
        // All other exceptions (non-http)
        println("Something broken", e)
    }
}

.awaitResult()

API based on sealed class Result:

fun Call<T>.awaitResult(): Result<T>
fun main(args: Array<String>) = runBlocking {
    // Wait (suspend) for Result
    val result: Result<User> = api.getUser("username").awaitResult()
    // Check result type
    when (result) {
        //Successful HTTP result
        is Result.Ok -> saveToDb(result.value)
        // Any HTTP error
        is Result.Error -> log("HTTP error with code ${result.error.code()}", result.error)
        // Exception while request invocation
        is Result.Exception -> log("Something broken", e)
    }
}

Also, Result has a few handy extension functions that allow to avoid when block matching:

fun main(args: Array<String>) = runBlocking {
    val result: User = api.getUser("username").awaitResult()
    
    //Return value for success or null for any http error or exception
    result.getOrNull()
    
    //Return result or default value
    result.getOrDefault(User("empty-user"))
    
    //Return value or throw exception (HttpException or original exception)
    result.getOrThrow()
    //Also supports custom exceptions to override original ones
    result.getOrThrow(IlleagalStateException("User request failed"))
}

All Result classes also implemented one or both interfaces: ResponseResult and ErrorResult You can use them for access to shared properties of different classes from Result

fun main(args: Array<String>) = runBlocking {
  val result: User = api.getUser("username").awaitResult()
  
  //Result.Ok and Result.Error both implement ResponseResult
  if (result is ResponseResult) {
      //And after smart cast you now have an access to okhttp3 Response property of result
      println("Result ${result.response.code()}: ${result.response.message()}")
  }
  
  //Result.Error and Result.Exception implement ErrorResult
  if (result is ErrorResult) {
      // Here yoy have an access to `exception` property of result
      throw result.exception
  }
}

Nullable body

To prevent unexpected behavior with a nullable body of response Call<Body?> extensions .await() and .awaitResult() are available only for non-nullable Call<Body> or platform Call<Body!> body types:

fun main(args: Array<String>) = runBlocking {
  val user: Call<User> = api.getUser("username")
  val userOrNull: Call<User?> = api.getUserOrNull("username")
  
  // Doesn't work, because User is nullable
  // userOrNull.await()
    
  // Works for non-nullable type
  try {
      val result: User = user.await()  
  } catch (e: NullPointerException) {
      // If body will be null you will get NullPointerException
  }
  
  // You can use .awaitResult() to catch possible problems with nullable body
  val nullableResult = api.getUser("username").awaitResult().getOrNull()
  // But type of body should be non-nullable
  // api.getUserOrNull("username").awaitResult()
  
  // If you still want to use nullable body to clarify your api
  // use awaitResponse() instead:
  val responseBody: User? = userOrNull.awaitResponse().body()
}

Parallel requests

Sometimes you want to run a few requests in parallel and don't want to wait for the previous request to make the next one. You can do that by wrapping calls with kotlinx.coroutines async()

fun main(args: Array<String>) = runBlocking {
  val users = listOf("user1", "user2", "user3")
      .map { username ->
        // Pass any coroutine context that fits better for your case
        // Coroutine Dispatcher also controls parallelism level 
        // for CommonPool parallelism is `availableProcessors - 1`
        // But you can use any custom dispatcher with any parallelism strategy
        async(CommonPool) {
            // Send request. We use `awaitResult()` here to avoid try/catch, 
            // but you can use `await()` and catch exceptions
            api.getUser(username).awaitResult() 
        }
      }
      // Handle results
      // in this example we get result or null in case of error and filter all nulls
      .mapNotNull {
        // Wait (suspend) for result of `async()` and get result of request
        // We must call first `await()` only when all `async` blocks are created for parallel requests
        it.await().getOrNull()
      }
}

You can read more about concurrent usage of async in the kotlinx.coroutines guide