Android编程之接口数据处理

时间:2021-08-08 18:30:03

  作为一枚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的具体配置:

@Singleton
public 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提供的也可根据具体需求自定义实现,处理结果大致是这样的:
Android编程之接口数据处理
我想多半app都会收集一些用户的信息,我项目中叫做KPI,我项目中是通过自定义拦截器做统一收集,也就是前文提到的SignInterceptor,具体实现如下:

@Singleton
public 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的扩展,代码如下:

@Singleton
public 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回调函数。

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

@Singleton
public 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,具体实现如下:

@Singleton
public 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