使用 Retrofit Android 对多个 API 进行常见成功/失败/错误处理的良好设计

Good design for common success / failure / error handling for multiple APIs using Retrofit Android

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

以下是我想考虑的场景。​​

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

我目前的实现不满足上述要求。 有什么方法可以使用 Retrofit 实现满足上述要求的 API 调用吗? 请为此建议我一个好的设计。

这是我当前的实现:

  1. ApiInterface.java - 这是一个包含不同 API 调用定义的接口。
  2. ApiClient.java - 获取改造客户端对象以调用 APIs.
  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 调用方组件,需要在回调中传递成功或失败结果我的意思是响应监听器。 我怎样才能做到这一点 ?有什么想法吗?

对于第3点,我对Retrofit知之甚少Authenticator。我认为这一点是合适的,但它需要同步调用才能使用刷新令牌获取新令牌。 如何对同步进行异步调用? (注意:此调用不是改造调用)

因此,借助此处改造 github 存储库中的官方示例:https://github.com/square/retrofit/blob/fbf1225e28e2094bec35f587b8933748b705d167/samples/src/main/java/com/example/retrofit/ErrorHandlingAdapter.java

ErrorHandlingAdapter 最接近您的要求,因为它允许您控制调用的排队、创建错误回调、自行调用错误回调。您是希望调用者执行某些操作,还是希望自己在一个地方或两者都处理它。

这就是您创建它的方法。请阅读内联评论以了解。

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);
                    }
                });
    }

您可能需要根据自己的需要改变一些东西,但我认为这可能是一个很好的参考,因为它在官方示例中。

通过在中心位置处理 success/failure 响应,我假设您想根据错误解析逻辑以及它如何创建 UI 副作用来摆脱重复的样板文件你的应用程序。

我可能建议通过为 Callback 创建一个自定义抽象来让事情变得非常简单,它根据您的领域逻辑为 success/failure 调用您的 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

您可以将您的实现用作单例来保存一组调用,并在注销时通知它取消所有正在进行的请求。请注意,请务必在集合中使用此类调用的弱引用,以避免 leaks/references 死调用。 (您可能还想集思广益,根据交易使用正确的集合或定期清理弱引用)

对于用例 (3),Authenticator 应该可以正常工作,因为您已经弄清楚了用法,有 2 个选项 -

  1. 将刷新令牌调用迁移到 OkHttp/Retrofit 并同步触发它
  2. 使用倒计时锁存器让身份验证器等待异步调用完成(超时设置为 connection/read/write 刷新令牌 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();
}

希望这对您的网络层实现有所帮助

1.处理成功/失败和错误响应,如 4xx、5xx 等 所有 API 都在一个中心位置。

创建以下两个 classes:

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)
    }
}

现在使用上面的ApiCallbackclass代替Retrofit的Callbackclass入队

2。想要取消入队请求,如果在注销的情况下请求已经发送,也停止处理响应(因为响应解析会修改应用程序的一些全局数据)

你不能中途停止处理响应,但你可以做的是不更新 ui 或 activity 如果有问题的 activity 不在前台,这可以是在 MVVM Architecture.

LiveData 的帮助下完成

3。如果访问令牌已过期并且从云收到 401 响应,它应该获取新令牌,然后使用新令牌再次自动调用 API。

像这样创建一个TokenAuthenticator.javaclass

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