协程在Retrofit上的使用

前情提要:很简单的用法,没啥可补充的,英文也很简单,自己看吧。

Suspend what you’re doing: Retrofit has now Coroutines support!

 

It official now! Retrofit 2.6.0 has been released with support for suspend functions.

This allows you to express the asynchrony of HTTP requests in an idiomatic惯用的 fashion for the Kotlin language.

Behind the scenes this behaves as if defined as fun user(...): Call and then invoked with Call.enqueue. You can also return Response for access to the response metadata.

To better understand how this works and how to migrate your current code (I know you will, just come back here when you notice how coincise and simple is the new syntax at the end of the post) let’s make an example app that… makes a simple network request!

In this example, we’ll use JSONPlaceholder, a fake REST API that’s very useful when you need a way to quickly test network requests.

We’ll use the /todos endpoint, that returns the json of a simple Todo object.

For reference:

GET https://jsonplaceholder.typicode.com/todos/1

will return:

{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

Test project set up

First of all, let’s configure Retrofit and the environment for our test. If you’re in an hurry, feel free to skip this part and go directly to the next paragraph.

First the only POJO we need is the data class for a single Todo:

data class Todo(
    val id: Int = 0,
    val title: String = "",
    val completed: Boolean = false
)

Then we write the Retrofit interface. Remember that the fake endpoint is:

GET https://jsonplaceholder.typicode.com/todos/1

So it will be:

interface Webservice {
    @GET("/todos/{id}")
    fun getTodo(@Path(value = "id") todoId: Int): Call
}

Now let’s implement the Retrofit builder that will return our webservice:

    val webservice by lazy {
        Retrofit.Builder()
            .baseUrl("https://jsonplaceholder.typicode.com/")
            .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
            .build().create(Webservice::class.java)
    }

Before Retrofit 2.6.0

If you never heard of coroutines and network calls together, here’s a pretty standard way to use Retrofit:

1. Our Repository makes the actual network request with Retrofit and returns a LiveData with the body of the response

class TodoRepository {
    var client: Webservice = RetrofitClient.webservice

    fun getTodo(id: Int): LiveData {
        val liveData = MutableLiveData()

        client.getTodo(id).enqueue(object: Callback{
            override fun onResponse(call: Call, response: Response) {
                if (response.isSuccessful) {

                    // When data is available, populate LiveData
                    liveData.value = response.body()
                }
            }

            override fun onFailure(call: Call, t: Throwable) {
                t.printStackTrace()
            }
        })

        // Synchronously return LiveData
        // Its value will be available onResponse
        return liveData
    }
}

Note that the LiveData value is setted as soon as we have a Response from Retrofit.

We’re not handling errors in this example, but of course you should do so. If you need inspiration灵感, have a look at the NetworkBoundResource implementation on the Andorid Architectural components example repo.

2. The ViewModel simply initializes the Repository, calls getTodo() from it and forwards live data to Activity:

class MainViewModel : ViewModel() {
    val repository: TodoRepository = TodoRepository()

    fun getFirstTodo(): LiveData {
        return repository.getTodo(1)
    }
}

3. The Activity observes live data, waiting for an available value when the network call is completed:

class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)

        showFirstTodo()
    }

    private fun showFirstTodo() {
        viewModel.getFirstTodo().observe(this, Observer {
            titleTextView.text = it.title
        })
    }
}

Right before Retrofit 2.6.0

You probably know that in the last months, Retrofit already had “unofficial” support for coroutines. You had to add retrofit2-kotlin-coroutines-adapter by Jake Wharton.

The adapter would convert a Retrofit Call in Deferred, on which you can invoke .await() in a CoroutineScope.

Since this is now deprecated, we’ll skip the implementation.

If you are already using this method, the migration to Retrofit 2.6.0 will be super easy.

In fact, here you can find how they migrated Plaid which was already using the deferred adapter.

Basically it’s a matter of replacing Deferred<>

@GET("api/me")
fun getUser(): Deferred

to suspend

@GET("api/me")
suspend fun getUser(): User

After Retrofit 2.6.0

So the magic now is that you can create suspend methods in your Retrofit interface and directly return your data object.

In our example:

fun getTodo(@Path(value = "id") todoId: Int): Call

will become

suspend fun getTodo(@Path(value = "id") todoId: Int): Todo

Repository

At this point, the logic in our Repository will be pretty simple. Seriously.

From this:

class TodoRepository {
    var client: Webservice = RetrofitClient.webservice

    fun getTodo(id: Int): LiveData {
        val liveData = MutableLiveData()

        client.getTodo(id).enqueue(object: Callback{
            override fun onResponse(call: Call, response: Response) {
                if (response.isSuccessful) {

                    // When data is available, populate LiveData
                    liveData.value = response.body()
                }
            }

            override fun onFailure(call: Call, t: Throwable) {
                t.printStackTrace()
            }
        })

        // Synchronously return LiveData
        // Its value will be available onResponse
        return liveData
    }
}

To just this:

class TodoRepository {
    var client: Webservice = RetrofitClient.webservice

    suspend fun getTodo(id: Int) = client.getTodo(id)
}

We don’t need to call enqueue() and implement callbacks anymore! But notice, now our repo method is suspend too and returns a Todo object.

To better understand what we will do next, I invite you to read my previous article about Coroutines and Lifecycle Architectural Components integration.

Viewmodel

Remember that the activity is expecting a LiveData to observe.

LiveData’s building block already provides a Coroutine Scope where to call suspend functions like the one in our repository. So let’s use that with the IO Dispatcher since we’re making a network call.

val firstTodo = liveData(Dispatchers.IO) {
    val retrivedTodo = repository.getTodo(1)

    emit(retrivedTodo)
}

The building block will automatically switch to the UI thread to update LiveData value when needed.

That’s it. We don’t even need to make a method to get the first todo like before. So our final ViewModel will be:

class MainViewModel : ViewModel() {
    val repository: TodoRepository = TodoRepository()

    val firstTodo = liveData(Dispatchers.IO) {
        val retrivedTodo = repository.getTodo(1)

        emit(retrivedTodo)
    }
}

The Activity code remains untouched as it should be.

As you can see, migrating your networking code to coroutines makes it much more concise and avoids unnecessary boilerplate with the callbacks.

There’re still some issues that are going to be fixed soon with the new implementation. For instance, Retrofit 2.6.0 suspend functions doesn’t support null response body types. This is to avoid an expansive dependency to kotlin-metadata-jvm. Work is being done on this front with a new light parser for Kotlin metadata. You can follow the progress on Github here.

(看完全文没讲网络返回异常处理)

For those of you wondering how to handle errors, you can do so in a try catch around your retrofit suspend calls. If you’re looking for a specific error code/message you can check/smart cast it to a retrofit HttpException. this is how i did it

try {
someRetrofitCall()
} catch (ex: Exception){
when(ex) {
is HttpException -> printLn(“${ex.code()}: ${ex.message()}“)
}
}

This was recommended by JakeWharton himself in this issue:
https://github.com/JakeWharton/retrofit2-kotlin-coroutines-adapter/issues/3

你可能感兴趣的:(协程,Retrofit)