Android编程之接口数据处理

来源:互联网 发布:java ocr身份证识别 编辑:程序博客网 时间:2024/04/25 22:00

  作为一枚android程序员,处理API数据接口可谓家常便饭,既然是一项不可少的事,那就把它做好。本篇博客的内容也是我近期做的项目中模块的一隅,代码组织的好与不好暂且不论,首先解决问题才是最应该被关注的,当然如果同学们有好的意见也随时跟我沟通。我先声明这篇博客不会去分析某个网络请求框架,也不会从头开始写一个网络请求框架,我们应该注重实效,并不是我不建议同学去分析某个知名的框架,恰恰相反我很鼓励同学们去分析精品框架源码并尝试写一些框架。不罗嗦了,下面进入本文正题。

制定规约

  首先要明白一点,我们客户端从提供数据的后台请求数据的规约必须先明确之,其中包括是采用SOAP Webservice还是Restful Webservice,采用XML协议作为数据交换格式还是采用JSON协议作为数据交换格式,接口返回的数据结构是什么样子的,具体每一个接口请求方式是什么。我目前的项目是采用Restful Webservice,交换数据格式为JSON。接口返回数据统一格式是这样的:

    {        status:xxx        msg:xxx        result:xxx    }

  当然这些数据是在一次请求成功后才能获得,先不考虑获取不到数据的情况,请求异常情况后文再做进一步探讨;status表明一次请求的状态,一般是指业务层级上的状态值,msg记录一次请求的说明信息文本,可直接在UI显示(建议采用这种方式,可后台灵活配置),result存储某一次请求所返回的业务实体或者业务实体集合,当然也有例外,暂且不议。

解决方案

  既然已经和后台制定好了规约,那接下来的工作就是客户端如果实施的问题。网络请求框架本文采用Square开源的Retrofit框架,对它还比较陌生的同学请阅读一下说明文档,接口返回数据解析采用的Google开源的Gson框架;关于如何配置Retrofit,我想同学同学们心里都有一个考量标准,以下是我目前项目的使用Retrofit的具体配置:

@Singletonpublic class ApiConnector {    private final Interceptor mSignInterceptor;    private Retrofit mRetrofit;    private OkHttpClient okHttpClient;    @Inject    public ApiConnector(Interceptor signInterceptor) {        mSignInterceptor = signInterceptor;        init();    }    private void init() {        HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();        loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);        okHttpClient = getHttpBuilder()                .addInterceptor(loggingInterceptor)                .addInterceptor(mSignInterceptor)                .connectTimeout(NetworkConfig.REQUEST_TIME_OUT_DURATION, TimeUnit.SECONDS)                .build();        mRetrofit = new Retrofit.Builder()                .baseUrl(getApiBaseUrl())                .client(okHttpClient)                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())                .addConverterFactory(GsonConverterFactory.create(new Gson()))                .build();    }    protected OkHttpClient.Builder getHttpBuilder() {        return new OkHttpClient.Builder();    }    protected String getApiBaseUrl() {        return NetworkConfig.BASE_API_URL;    }    public Retrofit getApiCreator() {        return mRetrofit;    }}

  首先构造函数中我们传入Interceptor某个实现对象,看到对象名signInterceptor就应该能猜到它是对请求进一步签名,接着分析init/0函数,在函数中我们又定义一个HttpLoggingInterceptor对象,看类名就该知道它是一个Http request日志拦截器,升级版的Retrofit2具有更高的可配置性,可将OkHttp作为Retrofit的Request client,OkHttp提供可配置的拦截器,HttpLoggingInterceptor是OkHttp提供的也可根据具体需求自定义实现,处理结果大致是这样的:

我想多半app都会收集一些用户的信息,我项目中叫做KPI,我项目中是通过自定义拦截器做统一收集,也就是前文提到的SignInterceptor,具体实现如下:

@Singletonpublic final class SigningInterceptor implements Interceptor {    private Context mContext;    private AccountEntity mAccountEntity;    private AuthToken mAuthToken;    @Inject    AccountCache mAccountCache;    @Inject    AuthTokenCache mAuthTokenCache;    @Inject    SigningInterceptor(Context context) {        mContext = context.getApplicationContext();    }    @Override    public Response intercept(Chain chain) throws IOException {        if (null == mAccountEntity)            mAccountEntity = mAccountCache.get();        if (null == mAuthToken)            mAuthToken = mAuthTokenCache.getAuthToken();        Request originalRequest = chain.request();        Request.Builder requestBuilder = originalRequest.newBuilder()                .addHeader(ParamConstants.KPI.CLIENT_TYPE, ParamConstants.Value.CLIENT)                .addHeader(ParamConstants.KPI.FIRST_SRC, ClientUtil.getFirstSrc(mContext                        , ParamConstants.KPI.CHANNEL_PLACE_HOLDER))                .addHeader(ParamConstants.KPI.LAST_SRC, ClientUtil.getLastSrc(mContext                        , ParamConstants.KPI.CHANNEL_PLACE_HOLDER))                .addHeader(ParamConstants.KPI.VERSION_NAME, ManifestUtils.getVersionName(mContext))                .addHeader(ParamConstants.KPI.VERSION_CODE, String.valueOf(ManifestUtils.getVersionCode(mContext)))                .method(originalRequest.method(), originalRequest.body());        if (null != mAccountEntity)            requestBuilder.addHeader(ParamConstants.KPI.ACCOUNT_ID, String.valueOf(mAccountEntity.getAccountId()));        if (null != mAuthToken)            requestBuilder.addHeader(ParamConstants.KPI.TOKEN, String.valueOf(mAuthToken.getToken()));        return chain.proceed(requestBuilder.build());    }}

  很容易发现Interceptor采用是责任链模式,Request.Builder也就是Builder模式,不熟悉的同学们请自行补脑,这里我们只需要在intercept/0函数中将所需要统计的KPI数据加入Request的Header中即可。大家先不要关注@Inject这个注解,以后我会说明用法,现在只需要知道它能通过配置把AccountCache、AuthTokenCache具体实现对象注入到需要它的对象中,也就是依赖注入。细心的同学应该能注意到在ApiConnector中getApiBaseUrl/0、getHttpBuilder/0函数访问级别是protected,而且会有疑惑为什么在baseUrl/0不直接使用NetworkConfig.BASE_API_URL而非要多此一举呢,我的项目所有账号系统都是通过Https方式传输,之所以ApiConnector不是final修饰getApiBaseUrl/0、getHttpBuilder/0函数访问级别是protected的原因所在了,我们看下ApiConnector的扩展,代码如下:

@Singletonpublic final class ApiSSLConnector extends ApiConnector {    private static List<byte[]> CERTIFICATES_DATA = new ArrayList<>();    private Context mContext;    @Inject    public ApiSSLConnector(Context context, Interceptor signInterceptor) {        super(signInterceptor);        mContext = context;        initHttpCerts();    }    private void initHttpCerts() {        try {            String[] certFiles = mContext.getAssets().list("certs");            if (certFiles != null) {                for (String cert : certFiles) {                    InputStream is = mContext.getAssets().open("certs/" + cert);                    addCertificate(is);                }            }        } catch (IOException e) {        }    }    private synchronized static void addCertificate(InputStream inputStream) {        if (null != inputStream) {            try {                int ava;                int len = 0;                ArrayList<byte[]> data = new ArrayList<>();                while ((ava = inputStream.available()) > 0) {                    byte[] buffer = new byte[ava];                    inputStream.read(buffer);                    data.add(buffer);                    len += ava;                }                byte[] buff = new byte[len];                int dstPos = 0;                for (byte[] bytes : data) {                    int length = bytes.length;                    System.arraycopy(bytes, 0, buff, dstPos, length);                    dstPos += length;                }                CERTIFICATES_DATA.add(buff);            } catch (IOException e) {            }        }    }    private static List<byte[]> getCertificatesData() {        return CERTIFICATES_DATA;    }    @Override    protected OkHttpClient.Builder getHttpBuilder() {        List<InputStream> certificates = new ArrayList<>();        List<byte[]> certsData = getCertificatesData();        if (certsData != null && !certsData.isEmpty()) {            for (byte[] bytes : certsData) {                certificates.add(new ByteArrayInputStream(bytes));            }        }        HttpsManager.SSLParams sslParams = HttpsManager.getSslSocketFactory(certificates, null, null);        OkHttpClient.Builder builder = new OkHttpClient.Builder();        builder.sslSocketFactory(sslParams.sSLSocketFactory, sslParams.trustManager)                .hostnameVerifier(new HttpsManager.UnSafeHostnameVerifier());        return builder;    }    @Override    protected String getApiBaseUrl() {        return NetworkConfig.SSL.BASE_API_URL;    }}

  这里需要注意一点,如果客户端和服务端采用非单向验证,那么getSslSocketFactory/3函数后两个参数必须要传入对应的参数信息。
了解Retrofit的同学会知道,使用Retrofit访问网络接口需要定义Service接口,该类型接口需要明确客户端需要请求哪些接口,哪种方式请求、请求参数、返回信息,下面是一个精简后的实例:

public interface AccountService {    @PUT("/user/sendSMS/{mobile}")    Observable<DataResponse<String>> getVerifyCode(@Path("mobile") String phoneNumber, @Body Map<String, String> verify);    @GET("/user/detail/{accountId}")    Observable<DataResponse<AccountEntity>accountDetail(@Path("accountId") int accountId);}

多了两个陌生的面孔DataResponse和Observable;先说下DataResponse,前文说道服务端返回的数据格式为:

    {        status:xxx        msg:xxx        result:xxx    }

我们不可能把每个业务bean定义都wrapper成这样子的吧:

public final class AccountWrapper{    private String status;    private String msg;    private Account result;}

显然这样做是不合理的,格式确定下来了,我们完全可以通过泛型解决这个问题,贴上干巴巴代码:

public final class DataResponse<T> {    @SerializedName("status")    private int status;    @SerializedName("msg")    private String message;    @SerializedName("result")    private T result;    public int getStatus() {        return status;    }    public void setStatus(int status) {        this.status = status;    }    public String getMessage() {        return message;    }    public void setMessage(String message) {        this.message = message;    }    public T getResult() {        return result;    }    public void setResult(T result) {        this.result = result;    }}

  Observable是RxJava提供的可观察对象,关于RxJava的介绍本文不做探究,本文默认同学们已经会RxJava的基本使用,我会更新关于RxJava分析和使用的博客。在此基础上我们需要对DataResponse进行处理,简化后的处理代码如下:

public final class ResponseFlatResult {    public static <T> Observable<T> flatResult(final DataResponse<T> result) {        return Observable.create(new Observable.OnSubscribe<T>() {            @Override            public void call(Subscriber<? super T> subscriber) {                switch (result.getStatus()) {                    case NetworkConstants.SUCCESS_CODE:                        if (null != result.getResult())                            subscriber.onNext(result.getResult());                        break;                    case NetworkConstants.FEED_NOT_FOUND_EXCEPTION:                        subscriber.onError(new FeedNotFoundException(result.getMessage()));                        break;                    default:                        subscriber.onError(new BusinessException(result.getMessage()));                }                subscriber.onCompleted();            }        });}

  flatResult/0函数用意很明确就是要在上层对返回数据处理后再往下分发,当status为200同时result不为空时notify订阅者,其它状态均视为异常情况,也就是触发订阅者的onError/0回调函数。

接下来如何触发一次网络请求呢?我采用的方式如下:

@Singletonpublic final class CloudAccountDataStore implements AccountDataStore {    private final AccountService mAccountService;    private final TokenInteractor mTokenInteractor;    private final AccountCache mAccountCache;    private final AuthTokenCache mAuthTokenCache;    @Inject    public CloudAccountDataStore(ApiSSLConnector apiConnector, TokenInteractor tokenInteracto , AccountCache accountCache            , AuthTokenCache authTokenCache) {        mAccountService = apiConnector.getApiCreator().create(AccountService.class);        mTokenInteractor = tokenInteractor;        mAccountCache = accountCache;        mAuthTokenCache = authTokenCache;    }    @Override    public Observable<AccountEntity> getAccountEntityDetail(final AccountParamProvider accountParamProvider) {        return mAccountService.accountDetail(accountParamProvider.getAccountId())                .flatMap(new Func1<DataResponse<AccountEntity>, Observable<AccountEntity>>() {                    @Override                    public Observable<AccountEntity> call(DataResponse<AccountEntity> accountEntityBaseResponse) {                        return ResponseFlatResult.flatResult(accountEntityBaseResponse);                    }                })                .onErrorResumeNext(mTokenInteractor.refreshTokenAndRetry(Observable.defer(new Func0<Observable<AccountEntity>>() {                    @Override                    public Observable<AccountEntity> call() {                        return mAccountService.accountDetail(accountParamProvider.getAccountId())                                .flatMap(new Func1<DataResponse<AccountEntity>, Observable<AccountEntity>>() {                                    @Override                                    public Observable<AccountEntity> call(DataResponse<AccountEntity> accountEntityBaseResponse) {                                        return ResponseFlatResult.flatResult(accountEntityBaseResponse);                                    }                                });                    }                })));    }}

  还是先看一下CloudAccountDataStore的构造函数,其接受必要的4个参数,ApiSSLConnector就是上文定义提供Retrofit对象的Connector,由于是Account的相关的DataStore所以这里使用的是ApiSSLConnector而不是ApiConnector,在解释TokenInteractor的用途前先简单说下我目前项目客户端和服务端的请求验证机制,Token同学们都应该了解,业务层面上客户端和服务器请求的令牌,服务端根据Token来判定请求的合法性。用户一次登录操作获取Token,之后的相关请求附带Token参数,但是Token又不可能再下次登录前永远有效,这样就失去Token的意图,所以我项目中采用Token和Retoken机制,过期时间分别位3天和15天,Token过期后用Retoken去请求一个新的Token,当Retoken也过期时客户端重新登录,用Retoken更新Token的过程肯定是用户无感知的,所以就引入了TokenInteractor,具体实现如下:

@Singletonpublic final class TokenInteractor {    private final AuthTokenService mAuthTokenService;    private final AuthTokenCache mAuthTokenCache;    @Inject    RxBus mRxBus;    @Inject    public TokenInteractor(ApiConnector apiConnector, AuthTokenCache authTokenCache) {        mAuthTokenService = apiConnector.getApiCreator().create(AuthTokenService.class);        mAuthTokenCache = authTokenCache;    }    public <T> Func1<Throwable, ? extends Observable<? extends T>> refreshTokenAndRetry(final Observable<T> toBeResumed) {        return new Func1<Throwable, Observable<? extends T>>() {            @Override            public Observable<? extends T> call(Throwable throwable) {                if (isHttp401Error(throwable)) {                    return refreshToken()                            .doOnError(new Action1<Throwable>() {                                @Override                                public void call(Throwable throwable) {                                    if (isHttp403Error(throwable))                                        mRxBus.post(new UnauthorizedEvent());                                }                            })                            .doOnNext(new Action1<AuthToken>() {                                @Override                                public void call(AuthToken authToken) {                                    mAuthTokenCache.saveAuthToken(authToken);                                }                            })                            .flatMap(new Func1<AuthToken, Observable<? extends T>>() {                                @Override                                public Observable<? extends T> call(AuthToken token) {                                    return toBeResumed;                                }                            });                }                return Observable.error(throwable);            }        };    }    private Observable<AuthToken> refreshToken() {        ReTokenParamProvider reTokenParamProvider = new ReTokenParamProvider();        reTokenParamProvider.reToken(mAuthTokenCache.getAuthToken().getReToken());        return mAuthTokenService.refreshAuthToken(reTokenParamProvider.getOptionalParam().getMap())                .flatMap(new Func1<DataResponse<AuthToken>, Observable<AuthToken>>() {                    @Override                    public Observable<AuthToken> call(DataResponse<AuthToken> authTokenBaseResponse) {                        return ResponseFlatResult.flatResult(authTokenBaseResponse);                    }                });    }    private boolean isHttp401Error(Throwable throwable) {        if (throwable instanceof HttpException) {            HttpException exception = (HttpException) throwable;            return exception.code() == 401;        } else            return false;    }    private boolean isHttp403Error(Throwable throwable) {        if (throwable instanceof HttpException) {            HttpException exception = (HttpException) throwable;            return exception.code() == 403;        } else            return false;    }}

  逻辑并不复杂,refreshTokenAndRetry/0 接收一个Observable对象,也就是针对某个请求的Observable对象,在遇到异常时对异常类型做判断,如果状态码401就去请求新的Token并恢复流,如果状态是403就通知客户端重新登录,回到CloudAccountDataStore继续分析getAccountEntityDetail/1函数,视角移到onErrorResumeNext操作上,这个RxJava操作符的作用是一旦源Observable遇到错误,onErrorResumeNext会把源Observable用一个新的Observable替换掉,可以看到mAccountService.accountDetail在新的Observable重新发起请求;继而是defer操作符,这个操作符与create、just、from等操作符一样,是创建类操作符,不过所有与该操作符相关的数据都是在订阅是才生效的,这就是避免了多余的订阅。

  文章到此也告一段落了,如果有不明白的地方可以联系我,接下的几天我会把项目继续分解讲解,会引入MVP,Dagger2等一些新的编程方式同时会一直伴随的讲解的就是Refactor,觉得对您有所帮助的话请多关注我的博客 0.0

2 0
原创粉丝点击