Kotlin coroutine +Retrofit the most elegant network request to use
1. Introduction
Retrofit’s support for coroutines is very rudimentary. To use it in Kotlin is not kotlin’s elegance
interface TestServer {
@GET("banner/json")
suspend fun banner(a): ApiResponse<List<Banner>>
}
// Implement parallel capture of abnormal network requests
fun oldBanner(a){
viewModelScope.launch {
// Traditional mode uses Retrofit with a try catch
val bannerAsync1 = async {
var result : ApiResponse<List<Banner>>? = null
kotlin.runCatching {
service.banner()
}.onFailure {
Log.e("banner",it.toString())
}.onSuccess {
result = it
}
result
}
val bannerAsync2 = async {
var result : ApiResponse<List<Banner>>? = null
kotlin.runCatching {
service.banner()
}.onFailure {
Log.e("banner",it.toString())
}.onSuccess {
result = it
}
result
}
bannerAsync1.await()
bannerAsync2.await()
}
}
Copy the code
One layer nested another layer, true unbearable. Kotlin should solve the problem in one line of code, in keeping with Kotlin’s elegance
After using this framework
interface TestServer {
@GET("banner/json")
suspend fun awaitBanner(a): Await<List<Banner>>
}
// Implement parallel capture of abnormal network requests
fun parallel(a){
viewModelScope.launch {
val awaitBanner1 = service.awaitBanner().tryAsync(this)
val awaitBanner2 = service.awaitBanner().tryAsync(this)
// Both interfaces are called together
awaitBanner1.await()
awaitBanner2.await()
}
}
Copy the code
2. Source address
GitHub
3. Look at Retrofit source code
So let’s look at Retrofit Create
public <T> T create(final Class<T> service) {
validateServiceInterface(service);
return(T) Proxy.newProxyInstance( service.getClassLoader(), new Class<? >[] {service}, new InvocationHandler() {private final Platform platform = Platform.get(a);private final Object[] emptyArgs = new Object[0];
@Override
public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args); } args = args ! =null ? args : emptyArgs;
return platform.isDefaultMethod(method)
? platform.invokeDefaultMethod(method, service, proxy, args)
: loadServiceMethod(method).invoke(args);// Specific call}}); }Copy the code
LoadServiceMethod (method).invoke(args) Enter this method to see what is called
We look at ‘adapt’ in suspenForResponse
@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call);/ / if the user does not set use DefaultCallAdapterFactory callAdapterFactory
//noinspection unchecked Checked by reflection inside RequestFactory.
Continuation<Response<ResponseT>> continuation =
(Continuation<Response<ResponseT>>) args[args.length - 1];
// See SuspendForBody for explanation about this try/catch.
try {
return KotlinExtensions.awaitResponse(call, continuation);
} catch (Exception e) {
returnKotlinExtensions.suspendAndThrow(e, continuation); }}}Copy the code
The coroutine calls the call directly. Specific call in DefaultCallAdapterFactory okhttp. Or in a user-defined callAdapterFactory
Therefore, we can customize the CallAdapterFactory to not perform network request access after the call, and then perform network request access when the user calls a specific method.
4. Customize the CallAdapterFactory
Retrofit makes a network request directly after the call, which makes it difficult to operate. We put control of network requests in our hands and we can do whatever we want.
class ApiResultCallAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
// Check if returnType is of Call
type
if(getRawType(returnType) ! = Call::class.java) return null
check(returnType is ParameterizedType) { "$returnType must be parameterized. Raw types are not supported" }
// Call
T = Await
val apiResultType = getParameterUpperBound(0, returnType)
// If you are not Await, this calladapter.factory process is compatible with normal mode
if(getRawType(apiResultType) ! = Await::class.java) return null
check(apiResultType is ParameterizedType) { "$apiResultType must be parameterized. Raw types are not supported" }
// retrieve the T in Await
that corresponds to the data type returned by the API
// val dataType = getParameterUpperBound(0, apiResultType)
return ApiResultCallAdapter<Any>(apiResultType)
}
}
class ApiResultCallAdapter<T>(private val type: Type) : CallAdapter<T, Call<Await<T>>> {
override fun responseType(a): Type = type
override fun adapt(call: Call<T>): Call<Await<T>> {
return ApiResultCall(call)
}
}
class ApiResultCall<T>(private val delegate: Call<T>) : Call<Await<T>> {
/** * This method is called by Retrofit code that handles the suspend method and passes in a callback. If you call callback.onResponse, If you call callback.onFailure, the suspend method throws an exception. So our implementation here is callback callback.onResponse, which will be okHTTP's call delegate * /
override fun enqueue(callback: Callback<Await<T> >) {
// Put the okHTTP call into AwaitImpl and return it directly without making a network request. The network request actually begins when the await of AwaitImpl is called
callback.onResponse(this@ApiResultCall, Response.success(delegate.toResponse()))
}
}
internal class AwaitImpl<T>(
private val call : Call<T>,
) : Await<T> {
override suspend fun await(a): T {
return try {
call.await()
} catch (t: Throwable) {
throw t
}
}
}
Copy the code
By customizing the callAdapter above, we defer the network request and do not request the network after calling Retrofit, only put the call required by the network request into await.
@GET("banner/json")
suspend fun awaitBanner(a): Await<List<Banner>>
Copy the code
We got Await<List> and did not make a network request. The call for okHttp is contained in this entity class.
In this case, we can define the following methods to catch exceptions
suspend fun <T> Await<T>.tryAsync(
scope: CoroutineScope,
onCatch: ((Throwable) - >Unit)? = null,
context: CoroutineContext = SupervisorJob(scope.coroutineContext[Job]), start: CoroutineStart = CoroutineStart.DEFAULT ): Deferred<T? > = scope.async(context, start) {try {
await()
} catch(e: Throwable) { onCatch? .invoke(e)null}}Copy the code
Similarly, requests that catch exceptions in parallel can be called in the following way, which is much more elegant and concise
/** * parallel async */
fun parallel(a){
viewModelScope.launch {
val awaitBanner1 = service.awaitBanner().tryAsync(this)
val awaitBanner2 = service.awaitBanner().tryAsync(this)
// Both interfaces are called together
awaitBanner1.await()
awaitBanner2.await()
}
}
Copy the code
At this point we find that the network request was successful and the data parsing failed. Because we have an await layer on the data. Certainly not parse success.
In the spirit of what is wrong and what is fixed, we customize Gson parsing
5. Customize Gson parsing
class GsonConverterFactory private constructor(private var responseCz : Class<*>,var responseConverter : GsonResponseBodyConverter, private val gson: Gson) : Converter.Factory() {
override fun responseBodyConverter(
type: Type, annotations: Array<Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
var adapter : TypeAdapter<*>? = null
// check whether it is Await
if (Utils.getRawType(type) == Await::class.java && type is ParameterizedType){
// retrieve T in Await
val awaitType = Utils.getParameterUpperBound(0, type)
if(awaitType ! =null){
adapter = gson.getAdapter(TypeToken.get(ParameterizedTypeImpl[responseCz,awaitType]))
}
}
// Not aWIAT normal resolution, compatible with normal mode
if(adapter == null){
adapter= gson.getAdapter(TypeToken.get(ParameterizedTypeImpl[responseCz,type]))
}
return responseConverter.init(gson, adapter!!) }}class MyGsonResponseBodyConverter : GsonResponseBodyConverter() {
override fun convert(value: ResponseBody): Any {
val jsonReader = gson.newJsonReader(value.charStream())
val data = adapter.read(jsonReader) as ApiResponse<*>
val t = data.data
val listData = t as? ApiPagerResponse<*>
if(listData ! =null) {
// If the return value list encapsulates the class, and is the first page with empty data, then give the empty exception to make the interface display empty
if (listData.isRefresh() && listData.isEmpty()) {
throw ParseException(NetConstant.EMPTY_CODE, data.errorMsg)
}
}
// errCode does not equal SUCCESS_CODE, an exception is thrown
if (data.errorCode ! = NetConstant.SUCCESS_CODE) {throw ParseException(data.errorCode, data.errorMsg)
}
returnt!! }}Copy the code
6. Use of this framework
Add the dependent
implementation "io.github.cnoke.ktnet:api:?"
Copy the code
Write a network request base class
open class ApiResponse<T>(
var data: T? = null.var errorCode: String = "".var errorMsg: String = ""
)
Copy the code
Implement com.cnoke.net.factory.GsonResponseBodyConverter
class MyGsonResponseBodyConverter : GsonResponseBodyConverter() {
override fun convert(value: ResponseBody): Any {
val jsonReader = gson.newJsonReader(value.charStream())
val data = adapter.read(jsonReader) as ApiResponse<*>
val t = data.data
val listData = t as? ApiPagerResponse<*>
if(listData ! =null) {
// If the return value list encapsulates the class, and is the first page with empty data, then give the empty exception to make the interface display empty
if (listData.isRefresh() && listData.isEmpty()) {
throw ParseException(NetConstant.EMPTY_CODE, data.errorMsg)
}
}
// errCode does not equal SUCCESS_CODE, an exception is thrown
if (data.errorCode ! = NetConstant.SUCCESS_CODE) {throw ParseException(data.errorCode, data.errorMsg)
}
returnt!! }}Copy the code
Making network requests
interface TestServer {
@GET("banner/json")
suspend fun awaitBanner(a): Await<List<Banner>>
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HeadInterceptor())
.addInterceptor(LogInterceptor())
.build()
val retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://www.wanandroid.com/")
.addCallAdapterFactory(ApiResultCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create(ApiResponse::class.java,MyGsonResponseBodyConverter()))
.build()
val service: TestServer = retrofit.create(TestServer::class.java)
lifecycleScope.launch {
val banner = service.awaitBanner().await()
}
Copy the code
If the request starts with a try, exceptions will be caught. If the request starts with a try, exceptions will not be caught.
fun banner(a){
lifecycleScope.launch {
TryAwait handles exceptions and returns null if an exception is returned
valawaitBanner = service.awaitBanner().tryAwait() awaitBanner? .let {for(banner in it){
Log.e("awaitBanner",banner.title)
}
}
/** * The exception will be thrown directly
val awaitBannerError = service.awaitBanner().await()
}
}
/** * serial await */
fun serial(a){
lifecycleScope.launch {
// Call the first interface await
val awaitBanner1 = service.awaitBanner().await()
// Call the second interface after the first interface completes
val awaitBanner2 = service.awaitBanner().await()
}
}
/** * parallel async */
fun parallel(a){
lifecycleScope.launch {
val awaitBanner1 = service.awaitBanner().async(this)
val awaitBanner2 = service.awaitBanner().async(this)
// Both interfaces are called together
awaitBanner1.await()
awaitBanner2.await()
}
}
Copy the code