当前位置:网站首页>Comparison of kotlin collaboration + retro build network request schemes
Comparison of kotlin collaboration + retro build network request schemes
2022-07-03 17:16:00 【Coating process】
author :FredYe
Recently used in research Kotlin coroutines + Retrofit Practice of making network request scheme , The plan will be introduced into the new project later ,Retrofit It's very easy to use , Basically, you can access it immediately by reading a document , Also in the github Found a large number of Demo See how others write , Read a lot of online articles , But I found that many articles are just a simple access Demo, Can't meet my current business needs . The following records the results of recent research and our use . First of all, we compare several schemes found on the Internet :
Scheme 1
Code from here This is a very good Kotlin coroutines + Retrofit Introduction article , The code is as follows :
- Definition of service
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}
- Retrofit Builder
object RetrofitBuilder {
private const val BASE_URL = "https://5e510330f2c0d300147c034c.mockapi.io/"
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build() //Doesn't require the adapter
}
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
- Some intermediate layers
class ApiHelper(private val apiService: ApiService) {
suspend fun getUsers() = apiService.getUsers()
}
class MainRepository(private val apiHelper: ApiHelper) {
suspend fun getUsers() = apiHelper.getUsers()
}
- stay ViewModel Get network data from
class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {
fun getUsers() = liveData(Dispatchers.IO) {
emit(Resource.loading(data = null))
try {
emit(Resource.success(data = mainRepository.getUsers()))
} catch (exception: Exception) {
emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
}
}
}
This code can communicate with the server , Meet the basic requirements , And there is also an exception handling mechanism . But there are the following problems :
- The granularity of exception handling is too large . If you need to handle different exceptions differently , It will be more troublesome .
- It needs to be done at every call place try…catch operation .
- No support from reponse Get the response header , http code Information . But there are a lot of them APP These treatments are usually not required , If you don't get the data , Just give a general prompt . So this scheme can be used directly in some cases .
Option two
from Github I found one on the Internet Demo, Link in here Compared with scheme 1 , The author is in BaseRepository Inside , Call the interface uniformly try…catch To deal with , So for the caller , You don't have to add every one try…catch 了 . The relevant code is as follows :
open class BaseRepository {
suspend fun <T : Any> apiCall(call: suspend () -> WanResponse<T>): WanResponse<T> {
return call.invoke()
}
suspend fun <T : Any> safeApiCall(call: suspend () -> Result<T>, errorMessage: String): Result<T> {
return try {
call()
} catch (e: Exception) {
// An exception was thrown when calling the API so we're converting this to an IOException
Result.Error(IOException(errorMessage, e))
}
}
suspend fun <T : Any> executeResponse(response: WanResponse<T>, successBlock: (suspend CoroutineScope.() -> Unit)? = null,
errorBlock: (suspend CoroutineScope.() -> Unit)? = null): Result<T> {
return coroutineScope {
if (response.errorCode == -1) {
errorBlock?.let {
it() }
Result.Error(IOException(response.errorMsg))
} else {
successBlock?.let {
it() }
Result.Success(response.data)
}
}
}
}
stay Repository That's what it says
class HomeRepository : BaseRepository() {
suspend fun getBanners(): Result<List<Banner>> {
return safeApiCall(call = {
requestBanners()},errorMessage = "")
}
private suspend fun requestBanners(): Result<List<Banner>> =
executeResponse(WanRetrofitClient.service.getBanner())
}
Option three
See this on the Internet Blog , The author uses a CallAdapter convert , take http Errors are converted into exceptions and thrown ( My own plan 1 is also based on this idea ). The core code is as follows :
class ApiResultCallAdapter<T>(private val type: Type) : CallAdapter<T, Call<ApiResult<T>>> {
override fun responseType(): Type = type
override fun adapt(call: Call<T>): Call<ApiResult<T>> {
return ApiResultCall(call)
}
}
class ApiResultCall<T>(private val delegate: Call<T>) : Call<ApiResult<T>> {
/** * The method will be Retrofit Handle suspend Method code call , And there came a callback, If you call back callback.onResponse, that suspend Method will successfully return * If you call back callback.onFailure that suspend Methods throw exceptions * * So our implementation here is always callback callback.onResponse, Only when the request succeeds, it returns ApiResult.Success object , * When it fails, it returns ApiResult.Failure object , In this way, the outside is calling suspend Method will not throw exceptions , Will definitely return to ApiResult.Success or ApiResult.Failure */
override fun enqueue(callback: Callback<ApiResult<T>>) {
//delegate It is used to make actual network requests Call<T> object , The success or failure of the network request will call back different methods
delegate.enqueue(object : Callback<T> {
/** * The network request returned successfully , The method is called back ( No matter what status code Is it right? 200) */
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
//http status yes 200+
// Worry here response.body() May be null( This situation has not been detected ), So I did something about this situation ,
// Another advantage of dealing with this situation is that we can ensure that we pass it on to ApiResult.Success The object of is not null, In this way, there is no need to judge the space when it is used outside
val apiResult = if (response.body() == null) {
ApiResult.Failure(ApiError.dataIsNull.errorCode, ApiError.dataIsNull.errorMsg)
} else {
ApiResult.Success(response.body()!!)
}
callback.onResponse(this@ApiResultCall, Response.success(apiResult))
} else {
//http status error
val failureApiResult = ApiResult.Failure(ApiError.httpStatusCodeError.errorCode, ApiError.httpStatusCodeError.errorMsg)
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}
}
/** * An exception occurred in the network request , The method is called back * * For network request success , But business failure , We will also be in the corresponding Interceptor Exception thrown in , This situation will also call back the method */
override fun onFailure(call: Call<T>, t: Throwable) {
val failureApiResult = if (t is ApiException) {
//Interceptor Li will pass throw ApiException To end the request directly meanwhile ApiException It will contain error messages
ApiResult.Failure(t.errorCode, t.errorMsg)
} else {
ApiResult.Failure(ApiError.unknownException.errorCode, ApiError.unknownException.errorMsg)
}
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}
})
}
...
}
The author has provided a Demo, If you want to use it , You need to add another wrapper class that returns data . The disadvantage of this scheme is that it cannot obtain header, Or that sentence , After all, this requirement is uncommon , You can ignore .
To sum up , The current online solutions may have limitations :
- If the server goes wrong , Unable to get specific error information . such as , If the server returns 401, 403, The network layer in these schemes cannot transmit these information .
- If the server passes header Passing data to the front end , These plans do not meet the needs .
For the two questions above , Let's consider how to improve the implementation of the framework .
Adjustment Ideas
We expect a network request scheme to meet the following objectives :
- Normal communication with the server
- You can get header data
- You can get the error information of the server (http code,message)
- Convenient exception handling
The adjusted scheme
The relevant dependent library versions of the following code
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation "com.squareup.retrofit2:converter-gson:2.8.1"
//Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"
- Convention common error types
We expect ApiException Can also return HTTP Code, So Appointment , Error message code from 20000 Start , So it won't be with HTTP Of Code There's a conflict .
ApiError
object ApiError {
var unknownError = Error(20000, "unKnown error")
var netError = Error(20001, "net error")
var emptyData = Error(20002, "empty data")
}
data class Error(var errorCode: Int, var errorMsg: String)
- Definition of return data
ApiResult.kt
Used to carry the returned data , Return normal business data when successful , Assemble in case of error errorCode, errorMsg, This data will be thrown up to the caller .
sealed class ApiResult<out T>() {
data class Success<out T>(val data: T):ApiResult<T>()
data class Failure(val errorCode:Int,val errorMsg:String):ApiResult<Nothing>()
}
data class ApiResponse<out T>(var errorCode: Int, var errorMsg: String, val data: T)
Scheme 1
This scheme supports obtaining HTTP Code, And return it to the caller , No support from HTTP Response Extract from header The data of .
- Definition of service
WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
suspend fun getBanner(): ApiResult<ApiResponse<List<Banner>>>
}
- Define a
ApiCallAdapterFactory.kt
In this, the response data will be filtered , For errors , Throw errors out .
class ApiCallAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
=
check(getRawType(returnType) == Call::class.java) {
"$returnType must be retrofit2.Call." }
check(returnType is ParameterizedType) {
"$returnType must be parameterized. Raw types are not supported" }
val apiResultType = getParameterUpperBound(0, returnType)
check(getRawType(apiResultType) == ApiResult::class.java) {
"$apiResultType must be ApiResult." }
check(apiResultType is ParameterizedType) {
"$apiResultType must be parameterized. Raw types are not supported" }
val dataType = getParameterUpperBound(0, apiResultType)
return ApiResultCallAdapter<Any>(dataType)
}
}
class ApiResultCallAdapter<T>(private val type: Type) : CallAdapter<T, Call<ApiResult<T>>> {
override fun responseType(): Type = type
override fun adapt(call: Call<T>): Call<ApiResult<T>> {
return ApiResultCall(call)
}
}
class ApiResultCall<T>(private val delegate: Call<T>) : Call<ApiResult<T>> {
override fun enqueue(callback: Callback<ApiResult<T>>) {
delegate.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val apiResult = if (response.body() == null) {
ApiResult.Failure(ApiError.emptyData.errorCode, ApiError.emptyData.errorMsg)
} else {
ApiResult.Success(response.body()!!)
}
callback.onResponse(this@ApiResultCall, Response.success(apiResult))
} else {
val failureApiResult = ApiResult.Failure(response.code(), response.message())
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
//Interceptor Li will pass throw ApiException To end the request directly meanwhile ApiException It will contain error messages
val failureApiResult = if (t is ApiException) {
ApiResult.Failure(t.errorCode, t.errorMessage)
} else {
ApiResult.Failure(ApiError.netError.errorCode, ApiError.netError.errorMsg)
}
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}
})
}
override fun clone(): Call<ApiResult<T>> = ApiResultCall(delegate.clone())
override fun execute(): Response<ApiResult<T>> {
throw UnsupportedOperationException("ApiResultCall does not support synchronous execution")
}
override fun isExecuted(): Boolean {
return delegate.isExecuted
}
override fun cancel() {
delegate.cancel()
}
override fun isCanceled(): Boolean {
return delegate.isCanceled
}
override fun request(): Request {
return delegate.request()
}
override fun timeout(): Timeout {
return delegate.timeout()
}
}
- stay Retrofit Specify... When initializing
CallAdapterFactory, The definition fileApiServiceCreator.ktas follows :
object ApiServiceCreator {
private const val BASE_URL = "https://www.wanandroid.com/"
var okHttpClient: OkHttpClient = OkHttpClient().newBuilder().build()
private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(ApiCallAdapterFactory())
.build()
fun <T> create(serviceClass: Class<T>): T = getRetrofit().create(serviceClass)
inline fun <reified T> create(): T = create(T::class.java)
}
- stay ViewModel The following is used in :
viewModelScope.launch {
when (val result = api.getBanner()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse<List<Banner>>
Log.i("API Response", "--------->data size:" + data.data.size)
}
is ApiResult.Failure -> {
Log.i("API Response","errorCode: ${
result.errorCode} errorMsg: ${
result.errorMsg}")
}
}
}
Option two
This scheme is based on Scheme 1 , Support from the HTTP Response Header Get data in .
- Definition of service
WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
fun getBanner2(): Call<ApiResponse<List<Banner>>>
}
Attention should be paid to getBanner2() There is no suspend keyword , Back to a Call Object of type , This is very important .
- Define a
CallWait.ktfile , byCallClass to add extension methodsawaitResult, This method has some logic and the aboveCallAdapterThe implementation in is similar .CallWait.ktThe document also draws on this paragraph Code
suspend fun <T : Any> Call<T>.awaitResult(): ApiResult<T> {
return suspendCancellableCoroutine {
continuation ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>?, response: Response<T>) {
continuation.resumeWith(runCatching {
if (response.isSuccessful) {
var data = response.body();
if (data == null) {
ApiResult.Failure(ApiError.emptyData.errorCode, ApiError.emptyData.errorMsg)
} else {
ApiResult.Success(data!!)
}
} else {
ApiResult.Failure(response.code(), response.message())
}
})
}
override fun onFailure(call: Call<T>, t: Throwable) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
if (t is ApiException) {
ApiResult.Failure(t.errorCode, t.errorMessage)
} else {
ApiResult.Failure(ApiError.netError.errorCode, ApiError.netError.errorMsg)
}
}
})
}
}
- Retrofit The initialization
Different from scheme 1 , stay Retrofit On initialization Unwanted Appoint CallAdapterFactory, The definition file ApiServiceCreator.kt
object ApiServiceCreator {
private const val BASE_URL = "https://www.wanandroid.com/"
var okHttpClient: OkHttpClient = OkHttpClient().newBuilder().build()
private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T = getRetrofit().create(serviceClass)
inline fun <reified T> create(): T = create(T::class.java)
}
- stay
ViewModelUse in , Basically consistent with method 1 , Just call it hereawaitResultMethod
viewModelScope.launch {
when (val result = api.getBanner2().awaitResult()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse<List<Banner>>
Log.i("API Response", "--------->data size:" + data.data.size)
}
is ApiResult.Failure -> {
Log.i("API Response","errorCode: ${
result.errorCode} errorMsg: ${
result.errorMsg}")
}
}
}
- If we want to go from reponse Of header Here's the data , have access to Retrofit Extension functions provided
awaitResponse, as follows :
try {
val result = api.getBanner2().awaitResponse()
// take HTTP Header Data in
Log.i("API Response", "-----header---->Server:" + result.headers().get("Server"))
if (result.isSuccessful) {
var data = result.body();
if (data != null && data is ApiResponse<List<Banner>>) {
Log.i("API Response", "--------->data:" + data.data.size)
}
} else {
// take HTTP Code
Log.i("API Response","errorCode: ${
result.code()}")
}
} catch (e: Exception) {
Log.i("API Response","exception: ${
e.message}");
}
Option three
If we use Java To achieve a set
- Defining services
public interface WanAndroidApiJava {
@GET("/banner/json")
public Call<NetResult<List<Banner>>> getBanner();
}
ApiExceptionTo encapsulate error messages
public class ApiException extends Exception {
private int errorCode;
private String errorMessage;
public ApiException(int errorCode, String message) {
this.errorCode = errorCode;
this.errorMessage = message;
}
public ApiException(int errorCode, String message, Throwable e) {
super(e);
this.errorCode = errorCode;
this.errorMessage = message;
}
public String getErrorMessage() {
return this.errorMessage;
}
public int getErrorCode() {
return this.errorCode;
}
interface Code {
int ERROR_CODE_DATA_PARSE = 20001;
int ERROR_CODE_SEVER_ERROR = 20002;
int ERROR_CODE_NET_ERROR = 20003;
}
public static final ApiException PARSE_ERROR = new ApiException(Code.ERROR_CODE_DATA_PARSE, " Data parsing error ");
public static final ApiException SERVER_ERROR = new ApiException(Code.ERROR_CODE_SEVER_ERROR, " Server response error ");
public static final ApiException NET_ERROR = new ApiException(Code.ERROR_CODE_NET_ERROR, " Network connection error ");
}
NetResultEncapsulate the response of the server
public class NetResult<T> {
private T data;
private int code;
private String errorMsg;
...// Omit get/set
}
- Customize a Callback To parse the data
public abstract class RetrofitCallbackEx<T> implements Callback<NetResult<T>> {
@Override
public void onResponse(Call<NetResult<T>> call, Response<NetResult<T>> response) {
// If the return is successful
if (response.isSuccessful()) {
NetResult<T> data = response.body();
// Back to right , And back end conventions , In the data returned code == 0 Represents business success
if (data.getCode() == 0) {
try {
onSuccess(data.getData());
} catch (Exception e) {
// Data parsing error
onFail(ApiException.PARSE_ERROR);
}
} else {
onFail(ApiException.SERVER_ERROR);
}
} else {
// Server request error
Log.i("API Response", "code:" + response.code() + " message:" + response.message());
onFail(ApiException.SERVER_ERROR);
}
}
@Override
public void onFailure(Call<NetResult<T>> call, Throwable t) {
onFail(ApiException.NET_ERROR);
}
protected abstract void onSuccess(T t);
protected abstract void onFail(ApiException e);
}
- Use
api.getBanner().enqueue(new RetrofitCallbackEx<List<Banner>>() {
@Override
protected void onSuccess(List<Banner> banners) {
if (banners != null) {
Log.i("API Response", "data size:" + banners.size());
}
}
@Override
protected void onFail(ApiException e) {
Log.i("API Response", "exception code:" + e.getErrorCode() + " msg:" + e.getErrorMessage() + " root cause: " + e.getMessage());
}
});
Other
- In the actual project , You may often encounter the need to be right HTTP Code For global processing , For example, when the server returns 401 When , Guide the user to the login page , This kind of overall interception is directly put into interceptor Just do it inside .
- The solution of the architecture is to meet the needs of the business , Here is just a survey of the business scenarios you encounter . Of course, there are usually more requirements in actual projects , For example, the switching of the environment leads to the difference of domain names , General configuration of network requests , Reporting of business exceptions, etc , A complete network request scheme needs to add more functions .
- Kotlin The language is very flexible , The use of extension functions can make the code very concise .Kotlin Not much used in our project , Not very proficient , coroutines + Retrofit There should be more elegant writing , Welcome to exchange .
边栏推荐
- Talk about several methods of interface optimization
- Design e-commerce spike
- Test your trained model
- C语言字符串练习
- C language string practice
- [try to hack] active detection and concealment technology
- kubernetes资源对象介绍及常用命令(四)
- RedHat 6.2 配置 Zabbix
- BYD and great wall hybrid market "get together" again
- On Lagrange interpolation and its application
猜你喜欢

建立自己的网站(23)

Static program analysis (I) -- Outline mind map and content introduction

聊聊接口优化的几个方法

新库上线 | CnOpenData中国保险机构网点全集数据

图之深度优先搜索

One brush 145 force deduction hot question-2 sum of two numbers (m)

SWM32系列教程4-端口映射及串口应用
The way of wisdom (unity of knowledge and action)

【JokerのZYNQ7020】DDS_ Compiler。
![[RT thread] NXP rt10xx device driver framework -- RTC construction and use](/img/19/91a9d84ba84f81ef125c33eb4007bc.png)
[RT thread] NXP rt10xx device driver framework -- RTC construction and use
随机推荐
Kotlin学习快速入门(7)——扩展的妙用
Redis: operation commands for list type data
Pools de Threads: les composants les plus courants et les plus sujets aux erreurs du Code d'affaires
Define a structure fraction to represent a fraction, which is used to represent fractions such as 2/3 and 5/6
一位普通程序员一天工作清单
Cross border e-commerce: advantages of foreign trade enterprises in overseas social media marketing
静态程序分析(一)—— 大纲思维导图与内容介绍
Kotlin学习快速入门(7)——扩展的妙用
Javescript variable declaration -- VaR, let, const
C language modifies files by line
[2. Basics of Delphi grammar] 1 Identifiers and reserved words
C language string practice
LeetCode13.罗马数字转整数(三种解法)
[combinatorics] recursive equation (definition of general solution | structure theorem of general solution of recursive equation without multiple roots)
Luogu: p2685 [tjoi2012] Bridge
29:第三章:开发通行证服务:12:开发【获得用户账户信息,接口】;(使用VO类包装查到的数据,以符合接口对返回数据的要求)(在多处都会用到的逻辑,在Controller中可以把其抽成一个共用方法)
29: Chapter 3: develop Passport Service: 12: develop [obtain user account information, interface]; (use VO class to package the found data to meet the requirements of the interface for the returned da
What is your income level in the country?
[RT thread] NXP rt10xx device driver framework -- Audio construction and use
On Lagrange interpolation and its application