【问题标题】:Good design for common success / failure / error handling for multiple APIs using Retrofit Android使用 Retrofit Android 对多个 API 进行常见成功/失败/错误处理的良好设计
【发布时间】:2021-08-08 10:28:58
【问题描述】:

我想以这样一种方式设计 API 调用,以便从一个地方轻松处理成功和失败响应(而不是为所有 API 编写相同的调用函数代码)

这是我想考虑的场景。​​

  1. 在一个中心位置处理所有 API 的成功/失败和错误响应,例如 4xx、5xx 等。
  2. 想要取消入队请求,并且在注销时如果请求已经发送也停止处理响应(因为响应解析会修改应用的一些全局数据)
  3. 如果访问令牌已过期并从云端收到 401 响应,则应获取新令牌,然后使用新令牌自动再次调用 API。

我当前的实现不满足上述要求。 有没有办法使用 Retrofit 实现满足上述要求的 API 调用? 请为此建议我一个好的设计。

这是我目前的实现:

  1. ApiInterface.java - 它是一个包含不同 API 调用定义的接口。
  2. ApiClient.java - 获取改造客户端对象以调用 API。
  3. 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 进行异步调用? (注意:此调用不是改造调用)

【问题讨论】:

    标签: android performance design-patterns architecture retrofit


    【解决方案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 个选项 -

    1. 将刷新令牌调用迁移到 OkHttp/Retrofit 并同步触发
    2. 使用倒计时锁存器使身份验证器等待异步调用完成(将超时设置为刷新令牌 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 ArchitectureLiveData 的帮助下完成。

    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中找到

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-14
      • 1970-01-01
      • 2010-10-19
      • 1970-01-01
      相关资源
      最近更新 更多