Android企业级架构-仿微信-网络架构

市场上的绝大多数APP,都离不开网络数据请求,从注册到登录、提交/拉取数据,都是我们必须要面对的问题,可见网络数据请求在APP产品中的重要性。那么网络请求在企业级架构中,应该怎样处理是比较合理的呢?这篇文章我们来聊一聊网络数据请求的架构处理。

可能有人会问:网上有太多合适的框架供我们选择和使用,比如原生的HttpClient、HttpURLConnection,Google在前两者上扩展的Volley,Square组织的OkHttp(+Retrofit组合)等等,都是很方便的网络请求框架,那我们为什么还要自己去探究,重复造轮呢?
PS:HttpClient因维护成本大,在2.3版本就不建议使用,6.0版本已经废弃,还在用的小伙伴要注意

原因很简单,在企业级开发过程中,最重要的指标是稳定,同时要控制更新、替换和后期维护的成本,这一指标对所有第三方框架都相同。另外还需要控制Apk文件的大小,不可以无限制的添加第三方库,或仅为一个小功能添加了一个庞大且没用的库。

说了这么多废话,下面扯正题

我们要做什么?是要自己写一套网络请求库?

No,网上有那么多写的牛X的网络库,我们怎么写也写不过他们。我们做的事,只为我们自己和我们的产品就够了。那应该做什么事呢?
封装
我们需要封装自己的网络层外壳,有了这个外壳,其它开发者不需要关注网络请求的具体实现,直接调用我们的API即可。以前Volley热门,我们可以使用Volley的实现,现在OkHttp比较火,我们可以使用OkHttp的实现,但是不管怎么换,都不会影响上层的使用。大大降低了更新、维护的成本。
时下最火的Retrofit是一款很时尚的封装框架,它的框架设计能力非一般人所能及,接口的设计与逻辑完全解耦,非常适合学习研究,只是今天火的是Retrofit,明天可能就是其它的XXX。所以笔者对于这种框架,仅停留在学习阶段,不会轻易使用,写一套最适合自己的,最普通的框架,对自己和团队的学习本成来说,都只有好处没有坏处

建议爱学习的朋友,仔细读一下Retrofit,网上有很多文章,我就不写了

原始底层封装

这节写封装的框架,是一套原始框架,其中不包涵任何的与需求相关的逻辑,所以今天这一节的代码,不管拿到哪个项目上,都可以适用。我们的网络连接层使用OkHttp3,优点大家都知道,不了解的去看一下。

网络数据请求是一套工作流,简单画了一副图,大家看一下

数据请求工作流

发起一个网络请求简单来说有5个节点:1. 发起请求,2. 连接请求,3. 请求响应,4. 数据解析,5. 返回结果。其中节点2我们使用OkHttp来帮我们做,那余下的4个节点,就是我们要做的事情了。

  1. 配置OkHttp3:打开OkHttp的GitHub地址我们看到OkHttp现在最新的版本是3.7.0,打开工程目录下的build.gradle文件,在ext中添加如下变量:
    配置OkHttp的版本号

    然后打开network目录下的build.gradle文件,在dependencies下添加如下引用
    compile "com.squareup.okhttp3:okhttp:$rootProject.ext.okhttpVersion"
    然后同步一下工程,将OkHttp的库下载到本地。
  2. 打开network目录下的src->main->java->com.monch.network,开始我们真正的Code之旅。
    整个文件目录并不算复杂,如下图所示:


    Java文件结构

    整个文件目录,是围绕图1的流程所示,从上到下分别是:executor(执行器)目录、AccountException(帐号异常)、ApiCallback(网络请求回调)、ApiRequest(网络请求)、ApiResponse(网络请求响应)、ApiResult(网络请求结果)、ArrayFactory(Array创建器)、Failed(错误类型枚举)
    在executor(执行器)目录下,包括IExecutor(执行器接口)、OkHttpCallback(OkHttp响应回调)、OkHttpClientFactory(OkHttpClient创建器)、OkHttpExecutor(OkHttp的执行器实现)、RequestUtils(请求工具类)

  3. 下面我们按照一个请求的流程来分解代码。
    请求的最开始是ApiRequest,代码如下
// 请求实例池
    private static final Pools.Pool<ApiRequest> POOL = new Pools.SynchronizedPool<>(30);

    // 生成ApiRequest实例
    public static ApiRequest obtain(Builder builder) {
        ApiRequest instance = POOL.acquire();
        if (instance == null) {
            instance = new ApiRequest(builder);
        }
        return instance;
    }

    // 释放ApiRequest实例
    public static void release(ApiRequest request) {
        if (request == null) return;
        IExecutor executor = request.executor;
        if (executor != null) {
            // 释放请求缓存
            executor.releaseCache(request);
        }
        request.url = null;
        // 在这里,我们将所有的ArrayMap都置为null,
        // 是因为之前的请求有可能已经将ArrayMap的空间增加的足够大
        // 为了避免浪费多余分配的空间,之后的每次使用我们都重新创建
        if (request.parameters != null) {
            request.parameters.clear();
            request.parameters = null;
        }
        if (request.headers != null) {
            request.headers.clear();
            request.headers = null;
        }
        if (request.files != null) {
            request.files.clear();
            request.files = null;
        }
        request.callback = null;
        request.charset = null;
        request.tag = null;
        POOL.release(request);
    }

    // 默认执行器
    private static IExecutor mDefaultExecutor = new OkHttpExecutor();
    // 默认编码方式
    private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    /* 请求类型 */
    public static final int GET = 0;
    public static final int POST = GET + 1;
    public static final int UPLOAD = POST + 1;
    public static final int DOWNLOAD = UPLOAD + 1;

    private String url;     // 请求URL
    private int type;       // 请求类型
    private ArrayMap<String, String> parameters;    // 参数集合
    private ArrayMap<String, String> headers;       // 请求头集合
    private ArrayMap<String, File> files;           // 上传文件集合
    private ApiCallback callback;                   // 请求回调
    private Charset charset;                        // 编码
    private Object tag;                             // 标记
    private IExecutor executor;                     // 执行器

    private ApiRequest(Builder builder) {
        this.url = builder.url;
        this.type = builder.type;
        this.parameters = builder.parameters;
        this.headers = builder.headers;
        this.files = builder.files;
        this.callback = builder.callback;
        this.charset = builder.charset != null ? builder.charset : DEFAULT_CHARSET;
        this.tag = builder.tag;
        this.executor = builder.executor != null ? builder.executor : mDefaultExecutor;
    }

    // 开始请求
    private void request() {
        switch (type) {
            case POST:
                executor.doPost(this);
                break;
            case UPLOAD:
                executor.doUpload(this);
                break;
            case DOWNLOAD:
                executor.doDownload(this);
                break;
            default:
                executor.doGet(this);
                break;
        }
    }

    public String getUrl() {
        return url;
    }

    public int getType() {
        return type;
    }

    public ArrayMap<String, String> getParameters() {
        return parameters;
    }

    public ArrayMap<String, String> getHeaders() {
        return headers;
    }

    public ArrayMap<String, File> getFiles() {
        return files;
    }

    public ApiCallback getCallback() {
        return callback;
    }

    public Charset getCharset() {
        return charset;
    }

    public Object getTag() {
        return tag;
    }

    public IExecutor getExecutor() {
        return executor;
    }

    public void cancel() {
        if (executor != null) {
            executor.cancel(this);
        }
    }

    public static Builder newBuilder() {
        return new Builder();
    }
    
    public static class Builder {
        private String url;
        private int type = GET;
        private ArrayMap<String, String> parameters;
        private ArrayMap<String, String> headers;
        private ArrayMap<String, File> files;
        private ApiCallback callback;
        private Charset charset;
        private Object tag;
        private IExecutor executor;
        private Builder(){}
        public Builder url(String url) {
            this.url = url;
            return this;
        }
        public Builder addParameter(String key, String value) {
            if (parameters == null) {
                parameters = ArrayFactory.createArrayMap();
            }
            parameters.put(key, value);
            return this;
        }
        public Builder addHeader(String key, String value) {
            if (headers == null) {
                headers = ArrayFactory.createArrayMap(1);
            }
            headers.put(key, value);
            return this;
        }
        public Builder addFile(String key, File file) {
            if (files == null) {
                files = ArrayFactory.createArrayMap(1);
            }
            files.put(key, file);
            return this;
        }
        public Builder callback(ApiCallback callback) {
            this.callback = callback;
            return this;
        }
        public Builder charset(Charset charset) {
            this.charset = charset;
            return this;
        }
        public Builder tag(Object tag) {
            this.tag = tag;
            return this;
        }
        public Builder executor(IExecutor executor) {
            this.executor = executor;
            return this;
        }
        public ApiRequest get() {
            this.type = GET;
            ApiRequest request = obtain(this);
            request.request();
            return request;
        }
        public ApiRequest post() {
            this.type = POST;
            ApiRequest request = obtain(this);
            request.request();
            return request;
        }
        public ApiRequest upload() {
            this.type = UPLOAD;
            ApiRequest request = obtain(this);
            request.request();
            return request;
        }
        public ApiRequest download() {
            this.type = DOWNLOAD;
            ApiRequest request = obtain(this);
            request.request();
            return request;
        }
    }

在企业级工程中,网络请求的需求量是巨大的,所以我们对每个ApiRequest都使用Pools.Pool,这是一个实例缓存池,避免在运行时重复创建大量的实例,了解过JVM的同学都知道,创建实例是一个比较耗时的过程,而且大量实例废弃后会引发GC频繁,造成卡顿。这里我要强调一点,优化都是从一点一滴做起的,在优化的过程中,我们学习的内容也能更深入。
obtain和release两个静态方法,是生成和释放ApiRequest实例的方法,用法大家可以看一下代码。
接下来就是一些属性的定义:
mDefaultExecutor:这个是静态变量,存放默认的网络请求执行器,在我们的框架中,默认为OkHttpExecutor的实现,如果某一天大家需要更改执行器,那么直接替换这里就可以了。
默认的编码方式是UTF-8,这个没什么好说,大家都这样。

接下来是请求类型的定义,共使用4种:GET、POST、UPLOAD、DOWNLOAD,网络请求还有一些其它的类型,比如PUT、DELETE等,但现在基本很少使用,所以这里不定义。
以上的这些属性都是静态的,在内存中单独存在,不属于任何一个实例。下面是一个请求真正需要的变量:请求地址、请求类型、参数、请求头、文件、回调、编码、标记、执行器。ApiRequest类的构造函数是私有的,强制使用obtain方法创建实例。
tag(标记)属性需要说明一下,这是一个起到上下文的属性,举个粟子:我们在Service中发起了一个获取好友列表的请求,假设这个接口比较费时,当数据还没有返回时,用户已经退出登录又换了一个帐号登录上,然后获取好友列表的请求才返回,如果这些数据按照当前已经换帐号的环境去处理,必然是错的,所以这个时候,大家就有必要做一下处理了。这个属性我们可以在发起请求时保存一个userId,在处理之前,对比一下当前的userId是否一至,当一至时再处理。
代码很简单,大家都应该能看懂。对外暴露的接口就是ApiRequest.Builder创建请求。

接下来看一下IExecutor接口

/**
     * 拉取请求
     * @param request
     */
    void doGet(ApiRequest request);

    /**
     * 提交请求
     * @param request
     */
    void doPost(ApiRequest request);

    /**
     * 上传请求
     * @param request
     */
    void doUpload(ApiRequest request);

    /**
     * 下载请求
     * @param request
     */
    void doDownload(ApiRequest request);

    /**
     * 释放缓存
     * @param request
     */
    void releaseCache(ApiRequest request);

    /**
     * 取消请求
     * @param request
     */
    void cancel(ApiRequest request);

    /**
     * 取消所有请求
     */
    void cancelAll();

这个接口定义了这些方法,此版本我们使用OkHttp来实现这些接口,如果有一天我们需要换成Volley连接,那么直接创建一个VolleyExecutor类,实现IExecutor接口即可。

OkHttpExecutor的具体实现如下

private OkHttpClient mOkHttpClient;
    private Map<ApiRequest, Call> mRequestCache = ArrayFactory.createConcurrentHashMap();

    public OkHttpExecutor() {
        mOkHttpClient = OkHttpClientFactory.getClient();
    }

    @Override
    public void doGet(ApiRequest request) {
        OkHttpClient client = mOkHttpClient;
        Charset charset = request.getCharset();
        String url = RequestUtils.makeUrl(request.getUrl(), request.getParameters(), charset);
        ApiCallback apiCallback = request.getCallback();
        if (apiCallback != null) {
            apiCallback.onStart(request);
        }
        Request.Builder builder = new Request.Builder();
        builder.url(url);
        builder.tag(request.getTag());
        builder.get();
        RequestUtils.makeHeader(builder, request.getHeaders(), charset);
        Call call = client.newCall(builder.build());
        call.enqueue(new OkHttpCallback(request));
        mRequestCache.put(request, call);
    }

    @Override
    public void doPost(ApiRequest request) {
        OkHttpClient client = mOkHttpClient;
        Charset charset = request.getCharset();
        ApiCallback callback = request.getCallback();
        if (callback != null) {
            callback.onStart(request);
        }
        Request.Builder builder = new Request.Builder();
        builder.url(request.getUrl());
        builder.tag(request.getTag());
        builder.post(formBody(request.getParameters(), charset));
        RequestUtils.makeHeader(builder, request.getHeaders(), charset);
        Call call = client.newCall(builder.build());
        call.enqueue(new OkHttpCallback(request));
        mRequestCache.put(request, call);
    }

    private RequestBody formBody(Map<String, String> params, Charset charset) {
        FormBody.Builder builder = new FormBody.Builder();
        if (params != null && !params.isEmpty()) {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                String key = RequestUtils.encode(entry.getKey(), charset);
                String value = RequestUtils.encode(entry.getValue(), charset);
                if (!TextUtils.isEmpty(key) && value != null) {
                    builder.addEncoded(key, value);
                }
            }
        }
        return builder.build();
    }

    @Override
    public void doUpload(ApiRequest request) {
        OkHttpClient client = mOkHttpClient;
        Charset charset = request.getCharset();
        ApiCallback callback = request.getCallback();
        if (callback != null) {
            callback.onStart(request);
        }
        Request.Builder builder = new Request.Builder();
        builder.url(request.getUrl());
        builder.tag(request.getTag());
        builder.post(formBody(request.getParameters(), request.getFiles(), charset));
        RequestUtils.makeHeader(builder, request.getHeaders(), charset);
        Call call = client.newCall(builder.build());
        call.enqueue(new OkHttpCallback(request));
        mRequestCache.put(request, call);
    }

    private static final String DEFAULT_CONTENT = "Content-Disposition";

    private RequestBody formBody(Map<String, String> params, Map<String, File> files, Charset charset) {
        MultipartBody.Builder builder = new MultipartBody.Builder();
        builder.setType(MultipartBody.FORM);
        if (params != null && !params.isEmpty()) {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                String key = RequestUtils.encode(entry.getKey(), charset);
                byte[] content = entry.getValue().getBytes(charset);
                builder.addPart(Headers.of(DEFAULT_CONTENT, getParamValue(key)),
                        RequestBody.create(MediaType.parse(charset.name()), content));
            }
        }
        if (files != null && !files.isEmpty()) {
            for (Map.Entry<String, File> entry : files.entrySet()) {
                String name = RequestUtils.encode(entry.getKey(), charset);
                File file = entry.getValue();
                String fileName = file.getName();
                builder.addPart(Headers.of(DEFAULT_CONTENT, getFileValue(name, fileName)),
                        RequestBody.create(MediaType.parse(guessMimeType(fileName)), file));
            }
        }
        return builder.build();
    }

    private static String guessMimeType(String path) {
        FileNameMap fileNameMap = URLConnection.getFileNameMap();
        String contentTypeFor = fileNameMap.getContentTypeFor(path);
        if (contentTypeFor == null) {
            contentTypeFor = "application/octet-stream";
        }
        return contentTypeFor;
    }

    private static String getParamValue(String name) {
        return "form-data; name=\"" + name + "\"";
    }

    private static String getFileValue(String name, String fileName) {
        return "form-data; name=\"" + name + "\"; filename=\"" + fileName + "\"";
    }

    @Override
    public void doDownload(ApiRequest request) {
        doGet(request);
    }

    @Override
    public void releaseCache(ApiRequest request) {
        if (request == null) return;
        if (mRequestCache.containsKey(request)) {
            mRequestCache.remove(request);
        }
    }

    @Override
    public void cancel(ApiRequest request) {
        if (request == null) return;
        if (mRequestCache.containsKey(request)) {
            Call call = mRequestCache.get(request);
            if (call != null && !call.isCanceled()) {
                call.cancel();
            }
            mRequestCache.remove(request);
        }
    }

    @Override
    public void cancelAll() {
        if (!mRequestCache.isEmpty()) {
            for (Call call : mRequestCache.values()) {
                if (call != null && !call.isCanceled()) {
                    call.cancel();
                }
            }
            mRequestCache.clear();
        }
    }

OkHttp的具体用法在这就不讲了,如果有不清楚的,去看一下OkHttp的使用即可。
这里有一点需要注意,就是我们的mRequestCache(请求缓存)变量,因为存在并发的问题,所以我这里定义为线程安全的ConcurrentHashMap。
请求的参数,在这里都已经做过Encoder处理,GET请求也会自动的将参数拼接到URL上,所以大家不需要担心。

OkHttpClient的赋值,是使用OkHttpClientFactory类的getClient方法,这个方法是获取了一个支持所有证书的SSL连接的实例,可用于https的请求。代码如下
···

private static final int TIMEOUT = 60;
private volatile static OkHttpClient mOkHttpClient;
static OkHttpClient getClient() {
    if (mOkHttpClient == null) {
        synchronized (OkHttpClientFactory.class) {
            if (mOkHttpClient == null) {
                try {
                    X509TrustManager trustManager = createInsecureTrustManager();
                    SSLSocketFactory sslSocketFactory = createInsecureSslSocketFactory(trustManager);
                    mOkHttpClient = new OkHttpClient.Builder()
                            .connectTimeout(TIMEOUT, TimeUnit.SECONDS)
                            .readTimeout(TIMEOUT, TimeUnit.SECONDS)
                            .writeTimeout(TIMEOUT, TimeUnit.SECONDS)
                            .sslSocketFactory(sslSocketFactory, trustManager)
                            .hostnameVerifier(createInsecureHostnameVerifier())
                            .build();
                } catch (Exception e) {
                    Log.e(TAG, "Get client error", e);
                    mOkHttpClient = new OkHttpClient.Builder()
                            .connectTimeout(TIMEOUT, TimeUnit.SECONDS)
                            .readTimeout(TIMEOUT, TimeUnit.SECONDS)
                            .writeTimeout(TIMEOUT, TimeUnit.SECONDS)
                            .build();
                }
            }
        }
    }
    return mOkHttpClient;
}

private static HostnameVerifier createInsecureHostnameVerifier() {
    return new HostnameVerifier() {
        @Override public boolean verify(String s, SSLSession sslSession) {
            return true;
        }
    };
}

private static SSLSocketFactory createInsecureSslSocketFactory(TrustManager trustManager) {
    try {
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, new TrustManager[] {trustManager}, new SecureRandom());
        return context.getSocketFactory();
    } catch (Exception e) {
        throw new AssertionError(e);
    }
}

/**
 * 信任所有证书
 */
private static X509TrustManager createInsecureTrustManager() {
    return new X509TrustManager() {
        @Override public void checkClientTrusted(X509Certificate[] chain, String authType) {
        }

        @Override public void checkServerTrusted(X509Certificate[] chain, String authType) {
        }

        @Override public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    };
}

···
接下来再看如下代码:
Call call = client.newCall(builder.build());
call.enqueue(new OkHttpCallback(request));
这是将每个具体请求加入到请求队列中,我们都创建了一个OkHttpCallback实例,用于请求响应的回调。代码如下

public class OkHttpCallback implements Callback {

    /** 主线程执行器 **/
    private static Handler mResponseHandler = new Handler(Looper.getMainLooper());
    private static Executor mResponsePoster = new Executor() {
        @Override
        public void execute(@NonNull Runnable command) {
            mResponseHandler.post(command);
        }
    };

    private ApiRequest apiRequest;

    public OkHttpCallback(ApiRequest apiRequest) {
        this.apiRequest = apiRequest;
    }

    @Override
    public void onFailure(Call call, IOException e) {
        // 请求失败,将任务抛回主线程执行
        mResponsePoster.execute(new FailedRunnable(apiRequest, call, e));
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        if (call.isCanceled()) {
            // 请求已终止
            mResponsePoster.execute(new FailedRunnable(apiRequest, call, null));
            return;
        }
        if (response == null) {
            // 请求未响应
            mResponsePoster.execute(new FailedRunnable(apiRequest, call,
                    new IOException("Response is null.")));
            return;
        }
        if (!response.isSuccessful()) {
            // 请求失败
            mResponsePoster.execute(new FailedRunnable(apiRequest, call,
                    new IOException("Unexpected code " + response)));
            return;
        }
        ApiCallback apiCallback = apiRequest.getCallback();
        if (apiCallback == null) return;
        ResponseBody body = response.body();
        try {
            // 填充请求响应数据
            ApiResponse apiResponse = ApiResponse.obtain();
            apiResponse.setCode(response.code());
            apiResponse.setCharset(apiRequest.getCharset());
            apiResponse.setBody(body.bytes());
            Headers headers = response.headers();
            for (String key : headers.names()) {
                apiResponse.addHeader(key, headers.get(key));
            }
            apiResponse.setTag(apiRequest.getTag());
            // 回调响应,运行在子线程,用于数据解析
            ApiResult apiResult = apiCallback.onResponse(apiResponse);
            // 释放响应数据
            ApiResponse.release(apiResponse);
            // 抛回主线程执行完成
            mResponsePoster.execute(new SuccessRunnable(apiRequest, call, apiResult));
        } catch (Exception e) {
            // 抛回主线程执行失败
            mResponsePoster.execute(new FailedRunnable(apiRequest, call, e));
        } finally {
            if (body != null) body.close();
        }
    }

    // 请求错误处理
    private static class FailedRunnable implements Runnable {
        private ApiRequest apiRequest;
        private Call call;
        private Throwable throwable;
        FailedRunnable(ApiRequest apiRequest, Call call, Throwable throwable) {
            this.apiRequest = apiRequest;
            this.call = call;
            this.throwable = throwable;
        }
        @Override
        public void run() {
            ApiCallback apiCallback = apiRequest.getCallback();
            if (apiCallback != null) {
                if (call.isCanceled()) {
                    apiCallback.onCancel();
                } else {
                    Failed f = Failed.OTHER;
                    if (throwable != null) {
                        if (throwable instanceof IOException) {
                            f = Failed.NETWORK_ERROR;       // 网络异常
                        } else if (throwable instanceof TimeoutException) {
                            f = Failed.TIMEOUT_ERROR;       // 请求超时
                        } else if (throwable instanceof JSONException) {
                            f = Failed.PARSE_ERROR;         // 数据解析异常
                        } else if (throwable instanceof AccountException) {
                            apiCallback.onAccountError();   // 帐户信息异常
                            return;
                        }
                    }
                    apiCallback.onFailure(f, throwable);
                }
            }
            ApiRequest.release(apiRequest);
        }
    }

    // 请求成功处理
    private static class SuccessRunnable implements Runnable {
        private ApiRequest apiRequest;
        private Call call;
        private ApiResult apiResult;
        SuccessRunnable(ApiRequest apiRequest, Call call, ApiResult apiResult) {
            this.apiRequest = apiRequest;
            this.call = call;
            this.apiResult = apiResult;
        }
        @Override
        public void run() {
            ApiCallback callback = apiRequest.getCallback();
            if (callback != null) {
                if (call.isCanceled()) {
                    callback.onCancel();
                } else {
                    callback.onComplete(apiResult);
                }
            }
            ApiResult.release(apiResult);
            ApiRequest.release(apiRequest);
        }
    }

}

这个类实现了okhttp3.Callback接口,需要实现两个方法onFailure和onResponse,大家应该都知道这两个方法的作用吧?
在onResponse回调方法中,我们创建了ApiResponse实例,将主要的响应数据填充进去,包括响应码、响应数据、响应头等信息,依靠这些数据,我们使用ApiCallback的回调,将这些数据抛给上层使用者去解析
ApiResult apiResult = apiCallback.onResponse(apiResponse);
然后将结果使用mResponsePoster抛回到主线程处理,至此,一个完整的请求流程完成了。
下面我们看一下ApiCallback都有哪些回调

public interface ApiCallback {

    /**
     * 请求开始回调,运行在当前线程
     * @param request 请求数据
     */
    void onStart(ApiRequest request);

    /**
     * 请求响应回调,运行在子线程,主要用于数据解析。
     * 在解析过程中,如果发现后台返回的错误为帐户异常,
     * 可直接抛出AccountException,在onAccountError回调中统一处理
     * @param response 响应数据
     * @return
     * @throws JSONException JSON解析异常
     * @throws AccountException 帐户登录异常
     */
    ApiResult onResponse(ApiResponse response) throws JSONException, AccountException;

    /**
     * 请求完成,运行在主线程,将解析后的结果返回
     * @param result 解析结果
     */
    void onComplete(ApiResult result);

    /**
     * 请求失败回调,运行在主线程
     * @param failed 错误类型
     * @param throwable 异常信息
     */
    void onFailure(Failed failed, Throwable throwable);

    /**
     * 帐户异常回调,运行在主线程,在onResponse回调中,如果接收到AccountException异常,会直接回调到这里
     */
    void onAccountError();

    /**
     * 请求取消回调,运行在主线程
     */
    void onCancel();

}

注释写的比较清楚,不再赘述。

整个封装的代码框架差不多就这些,此框架当前状态下,有着非常好的扩展性。当我需要替换成Volley,我只需要做到以下几点:

  1. 创建一个类叫VolleyExecutor,实现IExecutor接口,然后按照Volley的方式填充各个方法。
  2. 再创建一个叫VolleyCallback的类,将Volley的回调转换成我们的ApiCallback。
  3. 将ApiRequest类的mDefaultExecutor属性,默认实现替换成new VolleyExecutor();即可,完全不影响上层的任何使用。
    这只是一个简单的封装,我们也可以更彻底一点,做一个请求分发流,这样我们只需要使用OkHttp的连接功能,不过这样做太费事,暂时不考虑。

下面的代码,是现在状态的使用

ApiRequest req = ApiRequest.newBuilder()
                .url("https://www.baidu.com")
                .addParameter("key1", "value1")
                .addParameter("key2", "value2")
                .addHeader("header1", "headerValue1")
                .callback(new ApiCallback() {
                    @Override
                    public void onStart(ApiRequest request) {
                        // 请求开始
                    }

                    @Override
                    public ApiResult onResponse(ApiResponse response) throws JSONException, AccountException {
                        JSONObject jsonObject = new JSONObject(response.getString());
                        ApiResult result = ApiResult.obtain();
                        int code = jsonObject.optInt("code");
                        
                        // 假如我们与服务端的同学设定,code等于10的时候,说明帐号异常,那么处理代码如下:
                        if (code == 10) {
                            throw new AccountException();
                        }
                        
                        result.setCode(code);
                        result.setErrorMessage(jsonObject.optString("errorMsg"));
                        if (result.isSuccess()) {   // 当code为0时,表示成功
                            Object object = jsonObject.optString("test");
                            result.put("object", object);
                        }
                        return result;
                    }

                    @Override
                    public void onComplete(ApiResult result) {
                        if (result.isSuccess()) {
                            Object object = result.get("object");
                            if (object != null) {
                                Log.e("Test", object.toString());
                            }
                        }
                    }

                    @Override
                    public void onFailure(Failed failed, Throwable throwable) {
                        Toast.makeText(this, failed.error(), Toast.LENGTH_SHORT).show();
                    }

                    @Override
                    public void onAccountError() {
                        // 这里表示登录异常,我们可以将用户踢到登录页面
                    }

                    @Override
                    public void onCancel() {
                        // 请求取消的回调
                    }
                })
                .get();
        if (如果需要取消请求) {
            req.cancel();
        }

各位看官,是不是一看吓了一跳,我擦,这么费事还搞个屁呀。。。
说明,说明,说明。。。这是半成品,现在的状态,是扩展性非常好的状态,但是使用还不方便,在真正的工程使用时,我们还需要结合具体业务,做二次封装。比如我们所有的接口都需要上传用户的位置、版本号等信息,现在的状态是不方便的。

今天说到这,具体代码请移步GitHub查看。感谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • 一. Java基础部分.................................................
    wy_sure阅读 3,805评论 0 11
  • 从语言学角度来说,日本人是非常可怜的,因为他们只有5个元音——あ a(阿)、い i(衣)、う u(宇)、え e(诶...
    詹小虫阅读 1,420评论 3 5
  • 写在前面 十分果然地向前老板递交辞职信后,我选择跳进了或者说即将跳入另一个大坑:当一个互联网产品经理。作为一个-1...
    Jing_X阅读 2,371评论 0 3