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

Khu*_*hah 5 architecture performance android design-patterns retrofit

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

以下是我想考虑的场景。

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

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

这是我目前的实现:

  1. ApiInterface.java - 它是一个包含不同 API 调用定义的接口。
  2. ApiClient.java - 获取改造客户端对象以调用 API。
  3. ApiManager.java - 它具有调用 API 并解析其响应的方法。

接口接口.java

public interface ApiInterface {

    // Get Devices
    @GET("https://example-base-url.com" + "/devices")
    Call<ResponseBody> getDevices(@Header("Authorization) String token);

    // Other APIs......
}
Run Code Online (Sandbox Code Playgroud)

客户端程序

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

接口管理器

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
}
Run Code Online (Sandbox Code Playgroud)

更新 :

对于第一点,拦截器将有助于根据this全局处理 4xx、5xx 响应。但是拦截器将在 ApiClient 文件中并通知 UI 或 API 调用者组件,需要在回调中传递成功或失败结果我的意思是响应侦听器。我怎样才能做到这一点 ?任何的想法 ?

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

rah*_*cho 5

通过在中心位置处理成功/失败响应,我假设您希望根据错误解析逻辑以及它如何为您的应用程序创建 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

For use-case (2), to be able to cancel all enqueued calls upon a global event like logout you'd have to keep a reference to all such objects. Fortunately, Retrofit supports plugging in a custom call factory okhttp3.Call.Factory

You could use your implementation as a singleton to hold a collection of calls and in the event of a logout notify it to cancel all requests in-flight. Word of caution, do use weak references of such calls in the collection to avoid leaks/references to dead calls. (also you might want to brainstorm on the right collection to use or a periodic cleanup of weak references based on the transactions)

For use-case (3), Authenticator should work out fine since you've already figured out the usage there are 2 options -

  1. Migrate the refresh token call to OkHttp/Retrofit and fire it synchronously
  2. Use a count-down latch to make the authenticator wait for the async call to finish (with a timeout set to connection/read/write timeout for the refresh token API call)

Here's a sample implementation:

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();
}
Run Code Online (Sandbox Code Playgroud)

I hope this helps with your network layer implementation