本系列:
- 《從零開始的Android新專案(1):架構搭建篇》
- 《從零開始的Android新專案(2):Gradle 篇》
- 《從零開始的Android新專案(3):誰告訴你MVP和MVVM是互斥的》
- 《從零開始的Android新專案(4):Dagger2 篇》
如期而至的Repository篇,內部實現則由Realm、Retrofit,以及記憶體級LruCache組成。
Repository,顧名思義,即倉庫,向上層遮蔽了資料來源和內部實現細節,不需要了解貨物來源,只需要拿走就行了。
由於篇幅問題,將分為上下兩篇,本篇主要介紹Retrofit的應用和Repository層組裝,下篇會講解本地快取(包括Realm和記憶體快取)以及基於異常的設計。
Why Repository
首先,為什麼我們需要Repository層呢?一言以蔽之,遮蔽細節。
上層(activity/fragment/presenter)不需要知道資料的細節(或者說 – 資料來源),來自於網路、資料庫,亦或是記憶體等等。如此,一來上層可以不用關心細節,二來底層可以根據需求修改,不會影響上層,兩者的分離用可以幫助協同開發。
舉些例子:
- 當現在是無網狀態,我希望列表能直接顯示上一次的資料,而不會是空頁面。
- 除非好友的使用者資料過期(比如超過一天),否則希望直接使用本地快取中的,但如果快取沒有,或者過期,則需要拉取並更新。
- 點贊後,即便請求還沒傳送或者沒有收到response,仍然希望顯示點贊後的狀態。
等等。
如果這些需求,我們都要實現在View或者Presenter中,就會導致充斥大量資料邏輯,目的不單一,難以維護。而Repository層就是來封裝這些邏輯的。
Overview
如圖,業務層只能看到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 {<a href="http://www.jobbole.com/members/57845349">@link</a> 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 {<a href="http://www.jobbole.com/members/57845349">@link</a> 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,以及統一的異常處理設計。
另外,打個小廣告,本司的新產品Crew已經在各大Android應用市場上線,專注於職場垂直社交。一搜和興趣相投的人聊天。iOS版本正在稽核中。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式