使用 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 Retrofit的转换器

Android Retrofit的转换器

Android Retrofit框架解析

Android 网络框架之Retrofit2使用详解及从源码中解析原理

Android Retrofit Multipart图片上传至webapi,客户端发送请求,服务端却收到两个请求的原因

Android 网络框架 Retrofit2.0介绍使用和封装