使用 Retrofit Android 对多个 API 进行常见成功/失败/错误处理的良好设计
Posted
技术标签:
【中文标题】使用 Retrofit Android 对多个 API 进行常见成功/失败/错误处理的良好设计【英文标题】:Good design for common success / failure / error handling for multiple APIs using Retrofit Android 【发布时间】:2021-08-08 10:28:58 【问题描述】:我想以这样一种方式设计 API 调用,以便从一个地方轻松处理成功和失败响应(而不是为所有 API 编写相同的调用函数代码)
这是我想考虑的场景。
-
在一个中心位置处理所有 API 的成功/失败和错误响应,例如 4xx、5xx 等。
想要取消入队请求,并且在注销时如果请求已经发送也停止处理响应(因为响应解析会修改应用的一些全局数据)
如果访问令牌已过期并从云端收到 401 响应,则应获取新令牌,然后使用新令牌自动再次调用 API。
我当前的实现不满足上述要求。 有没有办法使用 Retrofit 实现满足上述要求的 API 调用? 请为此建议我一个好的设计。
这是我目前的实现:
-
ApiInterface.java - 它是一个包含不同 API 调用定义的接口。
ApiClient.java - 获取改造客户端对象以调用 API。
ApiManager.java - 它具有调用 API 并解析其响应的方法。
ApiInterface.java
public interface ApiInterface
// Get Devices
@GET("https://example-base-url.com" + "/devices")
Call<ResponseBody> getDevices(@Header("Authorization) String token);
// Other APIs......
ApiClient.java
public class ApiClient
private static Retrofit retrofitClient = null;
static Retrofit getClient(Context context)
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory(), systemDefaultTrustManager())
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build();
retrofitClient = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttpClient)
.build();
ApiManager.java
public class ApiManager
private static ApiManager apiManager;
public static ApiManager getInstance(Context context)
if (apiManager == null)
apiManager = new ApiManager(context);
return apiManager;
private ApiManager(Context context)
this.context = context;
apiInterface = ApiClient.getClient(context).create(ApiInterface.class);
public void getDevices(ResponseListener listener)
// API call and response handling
// Other API implementation
更新:
对于第一点,拦截器将有助于根据this 在全球范围内处理 4xx、5xx 响应。 但是拦截器将在 ApiClient 文件中并通知 UI 或 API 调用者组件,需要在回调中传递成功或失败结果,我的意思是响应侦听器。 我怎样才能做到这一点 ?有什么想法吗?
第三点,我对 Retrofit Authenticator
知之甚少。我认为在这一点上它是合适的,但它需要同步调用才能使用刷新令牌获取新令牌。
如何对 synchronous 进行异步调用? (注意:此调用不是改造调用)
【问题讨论】:
【参考方案1】:通过在一个中心位置处理成功/失败响应,我假设您希望根据错误解析逻辑以及它如何为您的应用创建 UI 副作用来摆脱重复的样板。
我可能会建议通过为 Callback
创建一个自定义抽象来保持事情非常简单,它会根据您的域逻辑调用您的 API 以获取成功/失败。
这是用例 (1) 的一些相当简单的实现:
abstract class CustomCallback<T> implements Callback<T>
abstract void onSuccess(T response);
abstract void onFailure(Throwable throwable);
@Override
public void onResponse(Call<T> call, Response<T> response)
if (response.isSuccessful())
onSuccess(response.body());
else
onFailure(new HttpException(response));
@Override
public void onFailure(Call<T> call, Throwable t)
onFailure(t);
对于用例 (2),为了能够在注销等全局事件时取消所有排队的调用,您必须保留对所有此类对象的引用。幸运的是,Retrofit
支持插入自定义调用工厂okhttp3.Call.Factory
您可以将您的实现用作单例来保存调用集合,并在注销时通知它以取消所有正在进行的请求。请注意,请在集合中使用此类调用的弱引用以避免泄漏/引用死调用。 (您也可能想针对要使用的正确集合进行头脑风暴,或者根据事务定期清理弱引用)
对于用例 (3),Authenticator
应该可以正常工作,因为您已经弄清楚了用法,有 2 个选项 -
-
将刷新令牌调用迁移到 OkHttp/Retrofit 并同步触发
使用倒计时锁存器使身份验证器等待异步调用完成(将超时设置为刷新令牌 API 调用的连接/读/写超时)
这是一个示例实现:
abstract class NetworkAuthenticator implements Authenticator
private final SessionRepository sessionRepository;
public NetworkAuthenticator(SessionRepository repository)
this.sessionRepository = repository;
public Request authenticate(@Nullable Route route, @NonNull Response response)
String latestToken = getLatestToken(response);
// Refresh token failed, trigger a logout for the user
if (latestToken == null)
logout();
return null;
return response
.request()
.newBuilder()
.header("AUTHORIZATION", latestToken)
.build();
private synchronized String getLatestToken(Response response)
String currentToken = sessionRepository.getAccessToken();
// For a signed out user latest token would be empty
if (currentToken.isEmpty()) return null;
// If other calls received a 401 and landed here, pass them through with updated token
if (!getAuthToken(response.request()).equals(currentToken))
return currentToken;
else
return refreshToken();
private String getAuthToken(Request request)
return request.header("AUTHORIZATION");
@Nullable
private String refreshToken()
String result = null;
CountDownLatch countDownLatch = new CountDownLatch(1);
// Make async call to fetch token and update result in the callback
// Wait up to 10 seconds for the refresh token to succeed
try
countDownLatch.await(10, TimeUnit.SECONDS);
catch (InterruptedException e)
e.printStackTrace();
return result;
abstract void logout();
我希望这对您的网络层实现有所帮助
【讨论】:
非常感谢您的帮助。【参考方案2】:因此,在改造 github 存储库中的官方示例的帮助下:https://github.com/square/retrofit/blob/fbf1225e28e2094bec35f587b8933748b705d167/samples/src/main/java/com/example/retrofit/ErrorHandlingAdapter.java
ErrorHandlingAdapter
最接近您的要求,因为它可以让您控制调用的排队、创建错误回调、自行调用错误回调。无论您是希望调用者执行某项操作,还是希望自己在一个地方处理,或两者兼而有之。
所以这就是你可以创建它的方法。请阅读内联 cmets 以了解。
public final class ErrorHandlingAdapter
/**
* Here you'll decide how many methods you want the caller to have.
*/
interface MyCallback<T>
void success(Response<T> response);
void error(String s);
/**
* This is your call type
*/
interface MyCall<T>
void cancel();
void enqueue(MyCallback<T> callback);
@NotNull
MyCall<T> clone();
public static class ErrorHandlingCallAdapterFactory extends CallAdapter.Factory
@Override
public @Nullable
CallAdapter<?, ?> get(
@NotNull Type returnType, @NotNull Annotation[] annotations, @NotNull Retrofit retrofit)
if (getRawType(returnType) != MyCall.class)
return null;
if (!(returnType instanceof ParameterizedType))
throw new IllegalStateException(
"MyCall must have generic type (e.g., MyCall<ResponseBody>)");
Type responseType = getParameterUpperBound(0, (ParameterizedType) returnType);
Executor callbackExecutor = retrofit.callbackExecutor();
return new ErrorHandlingCallAdapter<>(responseType, callbackExecutor);
private static final class ErrorHandlingCallAdapter<R> implements CallAdapter<R, MyCall<R>>
private final Type responseType;
private final Executor callbackExecutor;
ErrorHandlingCallAdapter(Type responseType, Executor callbackExecutor)
this.responseType = responseType;
this.callbackExecutor = callbackExecutor;
@NotNull
@Override
public Type responseType()
return responseType;
@Override
public MyCall<R> adapt(@NotNull Call<R> call)
return new MyCallAdapter<>(call, callbackExecutor);
static class MyCallAdapter<T> implements MyCall<T>
private final Call<T> call;
private final Executor callbackExecutor;
MyCallAdapter(Call<T> call, Executor callbackExecutor)
this.call = call;
this.callbackExecutor = callbackExecutor;
@Override
public void cancel()
call.cancel();
@Override
public void enqueue(final MyCallback<T> callback)
if (!SomeCondition.myCondition)
// Don't enqueue the call if my condition doesn't satisfy
// it could be a flag in preferences like user isn't logged in or
// some static flag where you don't want to allow calls
return;
call.clone().enqueue(
new Callback<T>()
@Override
public void onResponse(@NotNull Call<T> call, @NotNull Response<T> response)
callbackExecutor.execute(() ->
int code = response.code();
if (code >= 200 && code < 300)
//success response
callback.success(response);
else if (code == 401)
// Unauthenticated so fetch the token again
getTheTokenAgain(callback);
else if (code >= 400 && code < 500)
//handle error the way you want
callback.error("Client error");
else if (code >= 500 && code < 600)
//handle error the way you want
callback.error("Server error");
else
//handle error the way you want
callback.error("Something went wrong");
);
@Override
public void onFailure(@NotNull Call<T> call, @NotNull Throwable t)
callbackExecutor.execute(() ->
if (t instanceof IOException)
callback.error("IOException");
else
callback.error("Some exception");
);
);
private void getTheTokenAgain(MyCallback<T> callback)
// Make the call to get the token & when token arrives enqueue it again
// Don't forget to put termination condition like 3 times, if still not successful
// then just log user out or show error
// This is just dummy callback, you'll need to make a
// call to fetch token
new MyTokenCallback()
@Override
public void onTokenArrived(String token)
//enqueue(callback); here
@Override
public void onTokenFetchFailed()
callbackExecutor.execute(() ->
callback.error("Counld't fetch token");
);
;
// This is for demo you should put it in success callback
SomeCondition.callCount++;
Log.d("MG-getTheTokenAgain", "Method called");
if (SomeCondition.callCount < 3)
enqueue(callback);
else
callbackExecutor.execute(() ->
callback.error("Counld't fetch token");
);
@NotNull
@Override
public MyCall<T> clone()
return new MyCallAdapter<>(call.clone(), callbackExecutor);
这是您插入此适配器的方式:
private void makeApiCall()
//This is just for demo to generate 401 error you won't need this
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
httpClient.addInterceptor(chain ->
Request request = chain.request().newBuilder()
.addHeader("Accept","application/json")
.addHeader("Authorization", "cdsc").build();
return chain.proceed(request);
);
Retrofit retrofit =
new Retrofit.Builder()
.baseUrl("http://httpbin.org/")
.addCallAdapterFactory(new ErrorHandlingAdapter.ErrorHandlingCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient.build())
.build();
HttpBinService service = retrofit.create(HttpBinService.class);
ErrorHandlingAdapter.MyCall<Ip> ip = service.getIp();
ip.enqueue(
new ErrorHandlingAdapter.MyCallback<Ip>()
@Override
public void success(Response<Ip> response)
Log.d("MG-success", response.toString());
@Override
public void error(String s)
Log.d("MG-error", s);
);
您可能需要根据自己的需要调整一些东西,但我认为这可能是一个很好的参考,因为它在官方示例中。
【讨论】:
非常感谢您的帮助。对于我目前的情况,选择的答案更合适。但是ErrorHandlingAdapter 也是一个很好的参考。将来会有所帮助。【参考方案3】:1.处理成功/失败和错误响应,如 4xx、5xx 等 所有 API 都集中在一处。
创建以下两个类:
ApiResponse.kt
class ApiResponse<T : Any>
var status: Boolean = true
var message: String = ""
var data: T? = null
ApiCallback.kt
abstract class ApiCallback<T : Any> : Callback<ApiResponse<T>>
abstract fun onSuccess(response: ApiResponse<T>)
abstract fun onFailure(response: ApiResponse<T>)
override fun onResponse(call: Call<ApiResponse<T>>, response: Response<ApiResponse<T>>)
if (response.isSuccessful && response.body() != null && response.code() == 200)
onSuccess(response.body()!!)
else // handle 4xx & 5xx error codes here
val resp = ApiResponse<T>()
resp.status = false
resp.message = response.message()
onFailure(resp)
override fun onFailure(call: Call<ApiResponse<T>>, t: Throwable)
val response = ApiResponse<T>()
response.status = false
response.message = t.message.toString()
onFailure(response)
现在使用上面的 ApiCallback 类代替 Retrofit 的 Callback 类来入队
2。如果在注销的情况下,如果请求已经发送,想要取消入队请求并停止处理响应(因为响应解析会修改应用程序的一些全局数据)
您不能在中途停止处理响应,但是如果相关活动不在前台,您可以做的是不更新 ui 或活动,这可以在 MVVM Architecture 的 LiveData
的帮助下完成。
3.如果访问令牌已过期并收到来自云端的 401 响应,则应获取新令牌,然后使用新令牌自动再次调用 API。
像这样创建一个TokenAuthenticator.java
类
public class TokenAuthenticator implements Authenticator
@Override
public Request authenticate(Proxy proxy, Response response) throws IOException
// Refresh your access_token using a synchronous api request
newAccessToken = service.refreshToken();
// Add new header to rejected request and retry it
return response.request().newBuilder()
.header(AUTHORIZATION, newAccessToken)
.build();
@Override
public Request authenticateProxy(Proxy proxy, Response response) throws IOException
// Null indicates no attempt to authenticate.
return null;
像这样将上述验证器的实例附加到OkHttpClient
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setAuthenticator(authAuthenticator);
最后,将okHttpClient
附加到Retrofit
实例,就像您已经完成的那样
有关身份验证器部分的更多信息可以在此答案here中找到
【讨论】:
以上是关于使用 Retrofit Android 对多个 API 进行常见成功/失败/错误处理的良好设计的主要内容,如果未能解决你的问题,请参考以下文章
Android 网络框架之Retrofit2使用详解及从源码中解析原理