從零開始的Android新專案5
如期而至的Repository篇,內部實現則由Realm、Retrofit,以及記憶體級LruCache組成。
Repository,顧名思義,即倉庫,向上層遮蔽了資料來源和內部實現細節,不需要了解貨物來源,只需要拿走就行了。
由於篇幅問題,將分為上下兩篇,本篇主要介紹Retrofit的應用和Repository層組裝,下篇會講解本地快取(包括Realm和記憶體快取)以及基於異常的設計。
Why Repository
首先,為什麼我們需要Repository層呢?一言以蔽之,遮蔽細節。
上層(activity/fragment/presenter)不需要知道資料的細節(或者說 - 資料來源),來自於網路、資料庫,亦或是記憶體等等。如此,一來上層可以不用關心細節,二來底層可以根據需求修改,不會影響上層,兩者的分離用可以幫助協同開發。
舉些例子:
- 當現在是無網狀態,我希望列表能直接顯示上一次的資料,而不會是空頁面。
- 除非好友的使用者資料過期(比如超過一天),否則希望直接使用本地快取中的,但如果快取沒有,或者過期,則需要拉取並更新。
- 點贊後,即便請求還沒傳送或者沒有收到response,仍然希望顯示點贊後的狀態。
等等。
如果這些需求,我們都要實現在View或者Presenter中,就會導致充斥大量資料邏輯,目的不單一,難以維護。而Repository層就是來封裝這些邏輯的。
Overview
如圖,業務層只能看到repository介面。
Retrofit
Retrofit是Android界網紅公司
關於Retrofit的實現機制啊簡介的,網上已經很多了,這裡我就不囉嗦了,官方文件見專案主頁。這裡主要講講實際專案中的應用實踐。
import
root build.gradle:
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:
dependencies {
compile rootProject.ext.libRetrofit
compile rootProject.ext.libRetrofitConverterGson
compile rootProject.ext.libRetrofitAdapterRxJava
compile rootProject.ext.libOkHttpLoggingInterceptor
}
OkHttpClient
自底向上地,我們需要一個OkHttpClient來設定給Retrofit,這裡作為例項,放出一段包含大部分你可能會用到的功能的Client建立程式碼,可以根據需要進行調整。
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非同步請求
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請求類如
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啦。
public static MrService getSynchronousInstance() {
if (mSyncInstance == null) {
synchronized (MrService.class) {
if (mSyncInstance == null) {
mSyncInstance = new MrService(false);
}
}
}
return mSyncInstance;
}
對應地,我們需要定義請求類,這裡我們需要使用Call<>去包一下最終解析物件的類。
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,良好的資料結構通常都會帶有狀態碼和對應資訊:
@SerializedName("status_no")
private int statusCode;
@SerializedName("status_msg")
private String statusMessage;
根據statusCode可以快速判斷是否出現錯誤,通常0或者某個正數為正確,負數則根據和伺服器的協定做不同處理。
這裡對Gson的bean,推薦使用外掛GsonFormat,生成起來很方便。
至於具體的資料,則有兩種方案,一是使用data作為key把具體資料套起來,內部則使用K/V進行儲存,保證不存在不規範的直接丟一個array在data裡面的情形。
二次的組合解析
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:
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));
}
});
}
呼叫則如:
@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
。
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來作例子:
public interface QiniuRepository {
Observable<QiniuToken> getQiniuUploadToken();
}
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封裝資料來源:
@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來保證在某些情況下可以強制從網路獲取資料。
@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版本正在稽核中。
2個字找到志趣相投的職場夥伴,秒搜陌生人同類,智慧自動破冰。多關鍵字疊加,高效率鎖定職場同僚。精準匹配興趣物件,超輕聊天,更能一鍵組建群聊,加入一群人的狂歡。
demo沒空寫了,反正我也沒混淆,直接反編譯來黑我吧。哈哈。有bug或者功能上的意見建議歡迎直接反饋給我。
歡迎關注我們的公眾號:魔都三帥
,歡迎大家來投稿~