如期而至的Repository篇,内部实现则由Realm、Retrofit,以及内存级LruCache组成。
Repository,顾名思义,即仓库,向上层屏蔽了数据来源和内部实现细节,不需要了解货物来源,只需要拿走就行了。
由于篇幅问题,将分为上下两篇,本篇主要介绍Retrofit的应用和Repository层组装,下篇会讲解本地缓存(包括Realm和内存缓存)以及基于异常的设计。
Why Repository
首先,为什么我们需要Repository层呢?一言以蔽之,屏蔽细节。
上层(activity/fragment/presenter)不需要知道数据的细节(或者说 - 数据源),来自于网络、数据库,亦或是内存等等。如此,一来上层可以不用关心细节,二来底层可以根据需求修改,不会影响上层,两者的分离用可以帮助协同开发。
举些例子:
当现在是无网状态,我希望列表能直接显示上一次的数据,而不会是空页面。
除非好友的用户数据过期(比如超过一天),否则希望直接使用本地缓存中的,但如果缓存没有,或者过期,则需要拉取并更新。
点赞后,即便请求还没发送或者没有收到response,仍然希望显示点赞后的状态。
等等。
如果这些需求,我们都要实现在View或者Presenter中,就会导致充斥大量数据逻辑,目的不单一,难以维护。而Repository层就是来封装这些逻辑的。
Retrofit
Retrofit是Android界网红公司Square所开发维护的一个HTTP网络库,目前最新版本是2.0.2(截止2016年4月30日)。其内部使用了自家的OkHttp。
关于Retrofit的实现机制啊简介的,网上已经很多了,这里我就不啰嗦了,官方文档见项目主页。这里主要讲讲实际项目中的应用实践。
import
root build.gradle:
1 2 3 4 5 6 7 8 9 | def retrofitVersion = "2.0.2" def okHttpVersion = '3.2.0' project.ext { libRetrofit = "com.squareup.retrofit2:retrofit:${retrofitVersion}" libRetrofitConverterGson = "com.squareup.retrofit2:converter-gson:${retrofitVersion}" libRetrofitAdapterRxJava = "com.squareup.retrofit2:adapter-rxjava:${retrofitVersion}" libOkHttpLoggingInterceptor = "com.squareup.okhttp3:logging-interceptor:${okHttpVersion}" } |
repository module的build.gradle:
1 2 3 4 5 6 | dependencies { compile rootProject.ext.libRetrofit compile rootProject.ext.libRetrofitConverterGson compile rootProject.ext.libRetrofitAdapterRxJava compile rootProject.ext.libOkHttpLoggingInterceptor } |
OkHttpClient
自底向上地,我们需要一个OkHttpClient来设置给Retrofit,这里作为实例,放出一段包含大部分你可能会用到的功能的Client创建代码,可以根据需要进行调整。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | private OkHttpClient getClient() { // log用拦截器 HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); // 开发模式记录整个body,否则只记录基本信息如返回200,http协议版本等 if (IS_DEV) { logging.setLevel(HttpLoggingInterceptor.Level.BODY); } else { logging.setLevel(HttpLoggingInterceptor.Level.BASIC); } // 如果使用到HTTPS,我们需要创建SSLSocketFactory,并设置到client SSLSocketFactory sslSocketFactory = null; try { // 这里直接创建一个不做证书串验证的TrustManager final TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[]{}; } } }; // Install the all-trusting trust manager final SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); // Create an ssl socket factory with our all-trusting manager sslSocketFactory = sslContext.getSocketFactory(); } catch (Exception e) { Logger.e(TAG, e.getMessage()); } return new OkHttpClient.Builder() // HeadInterceptor实现了Interceptor,用来往Request Header添加一些业务相关数据,如APP版本,token信息 .addInterceptor(new HeadInterceptor()) .addInterceptor(logging) // 连接超时时间设置 .connectTimeout(10, TimeUnit.SECONDS) // 读取超时时间设置 .readTimeout(10, TimeUnit.SECONDS) .sslSocketFactory(sslSocketFactory) // 信任所有主机名 .hostnameVerifier((hostname, session) -> true) // 这里我们使用host name作为cookie保存的key .cookieJar(new CookieJar() { private final HashMap<HttpUrl, List<Cookie>> cookieStore = new HashMap<>(); @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) { cookieStore.put(HttpUrl.parse(url.host()), cookies); } @Override public List<Cookie> loadForRequest(HttpUrl url) { List<Cookie> cookies = cookieStore.get(HttpUrl.parse(url.host())); return cookies != null ? cookies : new ArrayList<>(); } }) .build(); } |
如上包含了大部分你可能需要的特性,可以自由进行组合。
RxJava异步请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public static MrService getInstance() { if (mInstance == null) { synchronized (MrService.class) { if (mInstance == null) { mInstance = new MrService(); } } } return mInstance; } private MrService() { this(true); } private MrService(boolean useRxJava) { Retrofit.Builder builder = new Retrofit.Builder() .baseUrl(IS_DEV ? API_DEV_URL : API_PRODUCT_URL) .addConverterFactory(GsonConverterFactory.create()) .client(getClient()); if (useRxJava) { builder.addCallAdapterFactory(RxJavaCallAdapterFactory.create()); } mRetrofit = builder.build(); } |
对应API请求类如
1 2 3 4 5 6 7 8 9 | public interface SystemApi { ... @FormUrlEncoded @POST("user/feedback") Observable<MrResponse> feedback(@Field("content") String content, @Field("model_name") String modelName, @Field("system_version") String systemVersion, @Field("img_keys") List<String> imageKeyList); } |
同步请求
有时候我们需要做同步请求,比如提供结果给一些第三方库,它们可能需要直接返回对应数据(像我最近碰到的融云….),而我们只需要拉数据同步返回,对其所在线程和调用事件均一脸懵逼。
这时候就需要创建一个同步的retrofit客户端,其实就是不要去使用RxJava的adapter啦。
1 2 3 4 5 6 7 8 9 10 | public static MrService getSynchronousInstance() { if (mSyncInstance == null) { synchronized (MrService.class) { if (mSyncInstance == null) { mSyncInstance = new MrService(false); } } } return mSyncInstance; } |
对应地,我们需要定义请求类,这里我们需要使用Call<>去包一下最终解析对象的类。
1 2 3 4 5 6 7 8 9 | public interface RongCloudApi { @FormUrlEncoded @POST("im/getGroupInfo") Call<MrResponse> getGroupInfoSynchronous(@Field("group_id") String groupId); @FormUrlEncoded @POST("user/nameCardLite") Call<MrResponse> getNameCardLiteSynchronous(@Field("uid") String userId); } |
数据格式解析
数据的解析当然是必不可少的一环了,常用格式对应的序列化库以retrofit官网为例:
Gson: com.squareup.retrofit2:converter-gson
Jackson: com.squareup.retrofit2:converter-jackson
Moshi: com.squareup.retrofit2:converter-moshi
Protobuf: com.squareup.retrofit2:converter-protobuf
Wire: com.squareup.retrofit2:converter-wire
Simple XML: com.squareup.retrofit2:converter-simplexml
Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars
部分高大上公司可能自己使用内部的二进制格式,自己实现ConverterFactory去解析就行了。
这里以最常用的json为例,使用GsonConverterFactory,良好的数据结构通常都会带有状态码和对应信息:
1 2 3 4 5 | @SerializedName("status_no") private int statusCode; @SerializedName("status_msg") private String statusMessage; |
根据statusCode可以快速判断是否出现错误,通常0或者某个正数为正确,负数则根据和服务器的协定做不同处理。
这里对Gson的bean,推荐使用插件GsonFormat,生成起来很方便。
至于具体的数据,则有两种方案,一是使用data作为key把具体数据套起来,内部则使用K/V进行存储,保证不存在不规范的直接丢一个array在data里面的情形。
二次的组合解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class CommonResponse { @SerializedName("status_no") private int statusCode; @SerializedName("status_msg") private String statusMessage; @SerializedName("time") private long time; @SerializedName("data") public Object data; // setter and getter } |
二次组合的解析通过将创建一个通用的Response Bean来做泛解析,如果statusCode表明接口请求成功,则继续解析data:
1 2 3 4 5 6 7 8 9 10 11 12 | public static <T> Observable<T> extractData(Observable<MrResponse> observable, Class<T> clazz) { return observable.flatMap(response -> { if (response == null) { return Observable.error(new NetworkConnectionException()); } else if (response.getStatusCode() == ResponseException.STATUS_CODE_SUCCESS) { return Observable.just(mGson.fromJson(mGson.toJson(response.data), clazz)); } else { Logger.e(TAG, response.data); return Observable.error(new ResponseException(response)); } }); } |
调用则如:
1 2 3 4 | @Override public Observable<AlbumApiResult> listPhoto(String uid) { return RepositoryUtils.extractData(mAlbumApi.listPhoto(uid), AlbumApiResult.class); } |
所有接口都可以通过RepositoryUtils.extractData()
进行泛型调用。
如此一来,如果response为空,我们仅在statusCode正确时才会去解析具体的数据,否则抛出对应的异常(基于异常的数据层设计在下面会具体讲)。
单次的继承处理
上一种处理方式尽管看起来很优雅,但是存在一个问题,就是会重复解析,当statusCode正确时,会对data的object再次进行json处理。如果确实是error,比如statusCode为-1、-2这种,确实节省了开销,因为gson会去反射构造对应类的adapter,解析所有字段,创建对应的BoundField。
但考虑到大部分情况下还是正确的response居多,所以也可以使用继承的结构,我们创建BaseResponse
存放通用字段,其他所有Gson Bean则继承该BaseResponse
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class BaseResponse { @SerializedName("status_no") private int statusCode; @SerializedName("status_msg") private String statusMessage; @SerializedName("time") private long time; // setter and getter } public class ConcreteResponse extends BaseResponse { @SerializedName("other_fields") private String otherFields; // ... } |
对应的判断和error抛出可以参照上小节的,这里就不赘述了。
Repository层组装实现
组装即根据组合各个数据源,如此又分为直接在实现方法中组合结果,亦或是通过DataStoreFactory进行封装。根据复杂度和个人喜好而定,毕竟使用后者需要新增好多类,相对来说有一点重。
基于接口的设计实现
拿一个最简单的repository,七牛Repository来作例子:
1 2 3 | public interface QiniuRepository { Observable<QiniuToken> getQiniuUploadToken(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class QiniuDataRepository implements QiniuRepository { @Inject protected QiniuApi mQiniuApi; @Inject public QiniuDataRepository() { } @Override public Observable<QiniuToken> getQiniuUploadToken() { return RepositoryUtils.extractData(mQiniuApi.getQiniuUploadToken(), QiniuToken.class); } } |
DataStoreFactory
使用DataStoreFactory封装数据来源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | @Singleton public class UserDataStoreFactory { private final Context mContext; private final UserCache mUserCache; @Inject protected UserApi mUserApi; @Inject public UserDataStoreFactory(Context context, UserCache userCache) { if (context == null || userCache == null) { throw new IllegalArgumentException("Constructor parameters cannot be null!!!"); } mContext = context.getApplicationContext(); mUserCache = userCache; } /** * Create {@link UserDataStore} from a user id. */ public UserDataStore create(String userId) { UserDataStore userDataStore; if (!mUserCache.isExpired() && mUserCache.isCached(userId)) { userDataStore = new DiskUserDataStore(mUserCache); } else { userDataStore = createCloudDataStore(); } return userDataStore; } /** * Create {@link UserDataStore} to retrieve data from the Cloud. */ public UserDataStore createCloudDataStore() { return new CloudUserDataStore(mUserApi, mUserCache); } } |
老实说这样的话,一来要写很多方法和接口,二来通过Factory判断创建哪种DataStore还是挺麻烦的,比如用户主页数据我们可以判断,但登陆登出这些,就需要直接指定createCloudDataStore()
了,所以个人认为意义不大。
在实现方法中组合
如下是使用DBFlow和网络Api进行组合的一个list获取接口。
我们使用RxJava的concat组合2个Observable,前者从cache(数据库)获取数据,后者从网络Api获取数据,通常数据库当然会更快。我们还保留了一个参数isForceRefresh来保证在某些情况下可以强制从网络获取数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | @Override public Observable<List<OperationPositionWrapper>> getHome(final boolean isForceRefresh) { final Observable<List<OperationPositionWrapper>> fromCache = Observable.create( new Observable.OnSubscribe<List<OperationPosition>>() { @Override public void call(Subscriber<? super List<OperationPosition>> subscriber) { List<OperationPosition> dbCache = new Select().from(OperationPosition.class).queryList(); if (dbCache != null) { subscriber.onNext(dbCache); } subscriber.onCompleted(); } }) .map(new Func1<List<OperationPosition>, List<OperationPositionWrapper>>() { @Override public List<OperationPositionWrapper> call(List<OperationPosition> operationPositions) { return OperationPositionMapper.wrap(operationPositions); } }) .filter(new Func1<List<OperationPositionWrapper>, Boolean>() { @Override public Boolean call(List<OperationPositionWrapper> operationPositionWrappers) { return ListUtils.isNotEmpty(operationPositionWrappers); } }); final Observable<List<OperationPositionWrapper>> fromNetwork = RepositoryUtils.observableWithApi(new GetOperationPositionsForYouleHomeApi()) .map(new Func1<List<OperationPositionPO>, List<OperationPositionWrapper>>() { @Override public List<OperationPositionWrapper> call(List<OperationPositionPO> operationPositionList) { return OperationPositionMapper.transform(operationPositionList); } }) .doOnNext(new Action1<List<OperationPositionWrapper>>() { @Override public void call(List<OperationPositionWrapper> operationPositionWrappers) { if (ListUtils.isNotEmpty(operationPositionWrappers)) { new Delete().from(OperationPosition.class).queryClose(); } for (OperationPositionWrapper wrapper : operationPositionWrappers) { wrapper.getOperationPosition().save(); } } }); if (isForceRefresh) { return fromNetwork; } else { return Observable.concat(fromCache, fromNetwork); } } |
总结
本篇为Repository层的上篇,主要介绍了组合及Retrofit的应用。下篇将会讲述数据库,内存Cache,以及统一的异常处理设计。
热门评论
1