最喜歡干的事霉颠,莫過于拿著工資搭框架了。
其實這個框架已經(jīng)出來很久了荆虱,并不是什么新鮮玩意兒了蒿偎,只不過我一直沒有嘗試著去寫一篇內(nèi)容比較大的文章來分享,這次就賣弄一下怀读,希望各種大神輕噴诉位,有什么問題也希望各位大神不吝賜教。
Retrofit的接入
ApiService
首先Retrofit的框架架構(gòu)搭建其實比較簡單菜枷,因為Retrofit本身已經(jīng)極致簡單了苍糠。
/**
* Author : yizhihao (Merlin)
* Create time : 2017-08-23 15:48
* contact :
* 562536056@qq.com || yizhihao.hut@gmail.com
*/
public interface ApiService {
@GET("{url}")
Observable<ResponseBody> executeGet(
@Path("url") String url,
@QueryMap Map<String, String> maps);
@POST("{url}")
Observable<ResponseBody> executePost(
@Path("url") String url,
@QueryMap Map<String, String> maps);
@POST("{url}")
Observable<ResponseBody> executeCachePost(
@Path("url") String url,
@QueryMap Map<String, String> maps);
@POST("{url}")
Observable<ResponseBody> uploadFiles(
@Path("url") String url,
@Path("headers") Map<String, String> headers,
@Part("filename") String description,
@PartMap() Map<String, RequestBody> maps);
@Streaming
@GET
Observable<ResponseBody> downloadFile(@Url String fileUrl);
}
上面的代碼通過將接口返回類型通用化返回結(jié)合rxjava的Observable這樣我們就可以愉快的用rxjava來處理線程切換了。
Retrofit接口對象
public static Retrofit retrofit() {
return retrofit(sBaseUrl);
}
public static Retrofit retrofit(String baseUrl) {
return new Retrofit.Builder()
.baseUrl(baseUrl)
.client(getInstance().getHttpClient())//添加自定義OkHttpClient
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(GsonUtils.getInstance().getGson()))
.build();
}
public OkHttpClient getHttpClient() {
if (client == null) {
client = new OkHttpClient.Builder()
//.addNetworkInterceptor(newCacheNetworkInterceptor())
//日志啤誊,可以配置 level 為 BASIC / HEADERS / BODY
.addInterceptor(new LoggingInterceptor())
.connectTimeout(DEFAULT_TIME_OUT, TimeUnit.SECONDS)
//.cache(provideCache())
.retryOnConnectionFailure(true)
.build();
}
return client;
}
Retrofit管理類主要是整合okhttp進行必要的配置
緩存攔截器
細心的讀者可能發(fā)現(xiàn)了CacheNetworkInterceptor這個注釋的攔截器岳瞭,它的職責(zé)本來要添加的NetworkInterceptor是為了做緩存Hook的。
但是查閱了一些資料蚊锹,還有okhttp源碼瞳筏,其實okhttp本身是自帶緩存邏輯的,這套邏輯完全遵守RFC協(xié)議進行緩存控制的枫耳。很多人都去hook掉了這步乏矾。其實查閱源碼可以看到
從源碼中不難看出,我們在自定義cache的時候,okhttp會把自己的internalCache給廢棄掉钻心,而我們在okhttp的內(nèi)部攔截器中也會看到CacheInterceptor凄硼,這個類其實就是實現(xiàn)了okhttp的Cache-control。所以我并沒有選擇去攔截Response手動添加Cache-control進行緩存處理捷沸。當然大家要用我也攔不住摊沉,畢竟也挺方便的。
日志攔截器
LoggingInterceptor攔截器主要是為了打印請求發(fā)送和收到請求的Log.
public class LoggingInterceptor implements Interceptor {
private boolean debugMode = DebugConstant.isDebug;
@Override
public Response intercept(Chain chain) throws IOException {
if(!debugMode){
return chain.proceed(chain.request());
}
//這個chain里面包含了request和response痒给,所以你要什么都可以從這里拿
Request request = chain.request();
long t1 = System.nanoTime();//請求發(fā)起的時間
LogUtils.e(String.format("發(fā)送請求 %s on %s%n%s", request.url(), chain.connection(), request.headers()));
Response response = chain.proceed(request);
long t2 = System.nanoTime();//收到響應(yīng)的時間
//這里不能直接使用response.body().string()的方式輸出日志
//因為response.body().string()之后说墨,response中的流會被關(guān)閉,程序會報錯苍柏,我們需要創(chuàng)建出一
//個新的response給應(yīng)用層處理
ResponseBody responseBody = response.peekBody(1024 * 1024);
LogUtils.d(String.format("接收響應(yīng): [%s]" +
"\n %n返回json:【%100s】 " +
"\n請求執(zhí)行時間%.1fms" +
"\n%n%s",
response.request().url(),
responseBody.string(),
(t2 - t1) / 1e6d,
response.headers()));
return response;
}
}
加上日志攔截器之后log如下圖
看到打印出來的詳細的log有木有感覺很酸爽尼斧。
Rxjava的封裝
綁定Activity生命周期
對rxjava中的subcriber的封裝,這里主要是將activity的生命周期和subcriber綁定聯(lián)系起來试吁,當activity被finish的時候我們的subcriber也應(yīng)該dispose取消掉棺棵。
private CompositeDisposable disposables2Stop;// 管理Stop取消訂閱者者
private CompositeDisposable disposables2Destroy;// 管理Destroy取消訂閱者者
在baseaActivity中通過CompositeDisposable組合管理添加進來的Disposable。然后在ondestroy中進行統(tǒng)一取消熄捍,防止內(nèi)存泄漏烛恤。
@Override
protected void onDestroy() {
super.onDestroy();
if (disposables2Destroy == null) {
throw new IllegalStateException(
"onDestroy called multiple times or onCreate not called");
}
disposables2Destroy.dispose();
disposables2Destroy = null;
if (mDelegate != null) {
mDelegate.ondestroy();
mDelegate = null;
}
}
基類訂閱者BaseObserver
通用BaseObserver是繼承于rxjava的Observer,在錯誤回調(diào)中的代碼余耽,前半部分是獲取錯誤的堆棧進行打印的邏輯缚柏,后面是對各類錯誤的通用處理。
public void onError(Throwable e) {
if (BuildConfig.DEBUG) {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stacks = e.getStackTrace();
sb.append(e.getMessage());
sb.append("\n");
for (StackTraceElement stack : stacks) {
sb.append(stack.getMethodName());
sb.append("(");
sb.append(stack.getClassName());
sb.append(".java:");
sb.append(stack.getLineNumber());
sb.append(")");
sb.append("\n");
}
LogUtils.e("Retrofit", sb.toString());
}
mBaseImpl.dismissProgress();
if (e instanceof HttpException) { // HTTP錯誤
onException(ExceptionReason.BAD_NETWORK);
} else if (e instanceof ConnectException
|| e instanceof UnknownHostException) { // 連接錯誤
onException(ExceptionReason.CONNECT_ERROR);
} else if (e instanceof InterruptedIOException) { // 連接超時
onException(ExceptionReason.CONNECT_TIMEOUT);
} else if (e instanceof JsonParseException
|| e instanceof JSONException
|| e instanceof ParseException) { // 解析錯誤
onException(ExceptionReason.PARSE_ERROR);
} else {
onException(ExceptionReason.UNKNOWN_ERROR);
}
}
Observer中另一個最重要的結(jié)果回調(diào)onNext中對errcode進行過濾碟贾,因為我自己封裝的model層返回的BaseResponce是沒有errorCode的币喧,這個model后面會講到,當然我也可以自己給通用BaseResponse加上200的code但是總感覺這兩個邏輯還是不要耦合的好缕陕,萬一code變了我model也要改,所以我在我在BaseResponce中設(shè)置了一個變量fromCache用于標記返回結(jié)果為緩存粱锐。代碼如下:
@Override
public void onNext(@NonNull T tBaseResponce) {
LogUtils.d(tBaseResponce.errCode + " || from cache : " + tBaseResponce.fromCache);
if (tBaseResponce.errCode == 200 || tBaseResponce.fromCache) {
onSuccess(tBaseResponce);
} else {
onFail(tBaseResponce);
}
}
public class BaseResponse<T>{
@SerializedName("code")
public int errCode;
@SerializedName("msg")
public String errMsg;
@SerializedName("data")
public T realData;
/**
* 請求結(jié)果是否來自緩存
*/
public boolean fromCache = false;
public BaseResponse<T> setData(T data){
realData = data;
return this;
}
@Override
public String toString() {
return "BaseResponse{" +
"errCode='" + errCode + '\'' +
", errMsg='" + errMsg + '\'' +
", data=" + realData +
'}';
}
}
另外BaseObserver引用的BaseImpl是activity的抽象接口,托管了進度條和綁定了activity的生命周期的邏輯扛邑。
public abstract class BaseObserver<T extends BaseResponse> implements Observer<T> {
private BaseImpl mBaseImpl;
// Activity 是否在執(zhí)行onStop()時取消訂閱
private boolean isAddInStop = false;
private boolean needProgress = false;
public BaseObserver(BaseImpl mBaseImpl,boolean needProgress) {
this.needProgress = needProgress;
this.mBaseImpl = mBaseImpl;
}
@Override
public void onSubscribe(@NonNull Disposable d) {
if(needProgress) mBaseImpl.showProgress("加載中");
if (isAddInStop) { // 在onStop中取消訂閱
mBaseImpl.addRxStop(d);
} else { // 在onDestroy中取消訂閱
mBaseImpl.addRxDestroy(d);
}
}
@Override
public void onNext(@NonNull T tBaseResponce) {
LogUtils.d(tBaseResponce.errCode + " || from cache : " + tBaseResponce.fromCache);
if (tBaseResponce.errCode == 200 || tBaseResponce.fromCache) {
onSuccess(tBaseResponce);
} else {
onFail(tBaseResponce);
}
}
@Override
public void onError(Throwable e) {
if (BuildConfig.DEBUG) {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stacks = e.getStackTrace();
sb.append(e.getMessage());
sb.append("\n");
for (StackTraceElement stack : stacks) {
sb.append(stack.getMethodName());
sb.append("(");
sb.append(stack.getClassName());
sb.append(".java:");
sb.append(stack.getLineNumber());
sb.append(")");
sb.append("\n");
}
LogUtils.e("Retrofit", sb.toString());
}
mBaseImpl.dismissProgress();
if (e instanceof HttpException) { // HTTP錯誤
onException(ExceptionReason.BAD_NETWORK);
} else if (e instanceof ConnectException
|| e instanceof UnknownHostException) { // 連接錯誤
onException(ExceptionReason.CONNECT_ERROR);
} else if (e instanceof InterruptedIOException) { // 連接超時
onException(ExceptionReason.CONNECT_TIMEOUT);
} else if (e instanceof JsonParseException
|| e instanceof JSONException
|| e instanceof ParseException) { // 解析錯誤
onException(ExceptionReason.PARSE_ERROR);
} else {
onException(ExceptionReason.UNKNOWN_ERROR);
}
}
@Override
public void onComplete() {
if(needProgress) mBaseImpl.dismissProgress();
}
/**
* 請求成功
*
* @param response 服務(wù)器返回的數(shù)據(jù)
*/
abstract public void onSuccess(T response);
/**
* 服務(wù)器返回數(shù)據(jù)怜浅,但響應(yīng)碼不為200
*
* @param response 服務(wù)器返回的數(shù)據(jù)
*/
public void onFail(T response) {
String message = response.errMsg;
if (TextUtils.isEmpty(message)) {
ToastUtils.showShort(R.string.response_return_error);
} else {
ToastUtils.showShort(message);
}
}
/**
* 請求異常
*
* @param reason
*/
public void onException(ExceptionReason reason) {
switch (reason) {
case CONNECT_ERROR:
ToastUtils.showShort(R.string.connect_error, Toast.LENGTH_SHORT);
break;
case CONNECT_TIMEOUT:
ToastUtils.showShort(R.string.connect_timeout, Toast.LENGTH_SHORT);
break;
case BAD_NETWORK:
ToastUtils.showShort(R.string.bad_network, Toast.LENGTH_SHORT);
break;
case PARSE_ERROR:
ToastUtils.showShort(R.string.parse_error, Toast.LENGTH_SHORT);
break;
case UNKNOWN_ERROR:
default:
ToastUtils.showShort(R.string.unknown_error, Toast.LENGTH_SHORT);
break;
}
}
/**
* 請求網(wǎng)絡(luò)失敗原因
*/
public enum ExceptionReason {
/**
* 解析數(shù)據(jù)失敗
*/
PARSE_ERROR,
/**
* 網(wǎng)絡(luò)問題
*/
BAD_NETWORK,
/**
* 連接錯誤
*/
CONNECT_ERROR,
/**
* 連接超時
*/
CONNECT_TIMEOUT,
/**
* 未知錯誤
*/
UNKNOWN_ERROR,
}
}
Model層的封裝
邏輯流程圖
然后說下上面提到的model層,我定義了接口IRepository蔬崩。
這個model的主要邏輯是 :
首先判斷是否需要強制刷新恶座,如果不需要強制刷新則去數(shù)據(jù)庫緩存中查看是否含有對象的緩存,如果是網(wǎng)絡(luò)獲取判斷是否需要緩存沥阳。這里的邏輯主要由客戶端控制跨琳。
public interface IRepository<T> {
/**
* 用于gson解析,以及一些Logname的打印桐罕。
* @return
*/
Class getTClass();
Observable<T> getEntry(final String url, Map<String, String> queryMap, final boolean needCache, boolean forceRefresh);
Observable<T> getEntry(final String url, Map<String, String> queryMap);
T getCache(String url) throws Exception;
Observable<T> getEntryFromNet(String url, Map<String, String> queryMap, boolean needCache);
void saveCache(String url, T baseBeanList);
String getCacheKey(String url, Map<String, String> queryMap);
void clearCache();
}
model的實現(xiàn)
拿目前公司的restful接口數(shù)據(jù)格式類型舉例:
{
"code":200,
"msg":"請求成功",
"data":{
"count":10,
"game_list":[
{
"gameid":362938
}
]
}
}
可以看出BaseResponce返回的泛型T對應(yīng)的data數(shù)據(jù)還需要繼續(xù)解析脉让。所以以目前的IRepository<T>
public abstract class IDBFlowRespository<BeanContainer,DBBean> implements IRepository<BaseResponse<BeanContainer>>{
是代碼是不能很好的封裝滿足需求的桂敛,所以我定義了抽象類繼承IRepository。
定義了2個泛型BeanContainer和DBBean溅潜,數(shù)據(jù)庫的相關(guān)操作基本由DBBean泛型實例完成术唬,網(wǎng)絡(luò)層的解析由BeanContainer完成。
各司其職滚澜。GameContainer對應(yīng)的是上圖json的data粗仓,gameList對應(yīng)的是上圖json的game_list。當然如果有其他類型的restful結(jié)構(gòu)设捐,我只需要在定義對應(yīng)類型的repository抽象類就好了借浊,畢竟現(xiàn)在返回的restful接口的json格式非常局限滿世界也就那么幾種,所以不用擔心repository的擴展類太多的問題萝招。
而真正的Repository實例代碼非常少蚂斤,只需要繼承4個接口就能滿足上述定義的model接口的功能,如下:
public class GameBeanRespository extends DBListRepository<GameContainerBean,GameContainerBean.GameListBean> {
//用于Gson對泛型的解析
@Override
public Class getTClass() {
return GameContainerBean.class;
}
//用于DB抽象類獲取對數(shù)據(jù)庫的引用
@Override
public Class getTableClass() {
return GameContainerBean.GameListBean.class;
}
@Override
public List<GameContainerBean.GameListBean> mapContainer(GameContainerBean beanContainer) {
return beanContainer.gameList;
}
@Override
public GameContainerBean mapTableBean(List<GameContainerBean.GameListBean> gameListBeen) {
return new GameContainerBean(gameListBeen);
}
}
BaseModel是我對實體的抽象繼承的是DBflow的BaseModel可以進行數(shù)據(jù)庫的增刪改即寒,很方便橡淆。
public abstract class BaseModel extends com.raizlabs.android.dbflow.structure.BaseModel{
public static final String KEY = "keyUrl";
@Column(name = KEY)
public String keyUrl;
}
其中key是對每個bean對應(yīng)的數(shù)據(jù)庫增加的字段主要是用來根據(jù)url進行緩存查詢的。
其中key是由Url拼接上queryMap的參數(shù)組成母赵,邏輯如下:
public String getCacheKey(String url, Map<String, String> queryMap) {
StringBuilder sb = new StringBuilder();
sb.append(url);
if (queryMap != null && !queryMap.isEmpty()) {
Set<String> keys = queryMap.keySet();
sb.append("?");
for (String key : keys) {
sb.append(key).append("=").append(queryMap.get(key));
}
}
return sb.toString();
}
有個小問題,因為網(wǎng)絡(luò)數(shù)據(jù)獲取是從我們定義的retrofit通用接口中返回具滴,返回的對象是Obserable<ResponseBody>而我們的model接受的參數(shù)是Observable<BaseResponce<T>>凹嘲,等于是承包了GsonConvertFactory的工作,我們把返回的Observer通過rxjava的map轉(zhuǎn)成我們的Model對應(yīng)的的Observer類型就行了构韵。
@Override
public Observable<BaseResponse<Container>> getEntryFromNet(String url, Map<String, String> queryMap, boolean needCache) {
return HttpRequestFactory.retrofit().create(ApiService.class)
.executeGet(url,queryMap).map(new Function<ResponseBody, BaseResponse<Container>>() {
@Override
public BaseResponse<Container> apply(@NonNull ResponseBody responseBody) throws Exception {
return GsonUtils.getInstance().fromJson(responseBody.string(), GsonUtils.type(BaseResponse.class,getTClass()));
}
});
}
獲取model集合的的主要邏輯代碼塊如下:
@Override
public Observable<BaseResponse<Container>> getEntry(final String url, Map<String, String> queryMap, final boolean needCache, boolean forceRefresh) {
final String key = getCacheKey(url, queryMap);
//get cache
Observable<BaseResponse<Container>> fromCache = Observable.create(new ObservableOnSubscribe<BaseResponse<Container>>() {
@Override
public void subscribe(@NonNull ObservableEmitter<BaseResponse<Container>> e) throws Exception {
final BaseResponse<Container> cacheResponce = getCache(key);
if (cacheResponce != null) {
LogUtils.e("Cache hint | key = " + key);
cacheResponce.fromCache = true;
e.onNext(cacheResponce);
} else {
e.onComplete();
}
}
});
//save cache
Observable<BaseResponse<Container>> fromNet = getEntryFromNet(url, queryMap ,needCache).map(new Function<BaseResponse<Container>, BaseResponse<Container>>() {
@Override
public BaseResponse<Container> apply(@NonNull BaseResponse<Container> tBaseResponse) throws Exception {
if (needCache) saveCache(key, tBaseResponse);
return tBaseResponse;
}
});
if (forceRefresh) {
return fromNet;
}
return Observable.concat(fromCache, fromNet)
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
可以看到周蹭,Cache命中的時候會將里面BaseResponce的fromCache標記為true。這樣就能和上面的提到的
BaseObserver對應(yīng)的onNext邏輯相吻合了疲恢。
測試實例
new GameBeanRespository()
.getEntry(new UrlConstant.Builder(false).shuffix().game().list().build()//參數(shù)url
,new RxMap()
.put("page","2")
.put("offset","10")
.build())//參數(shù)query maps
.subscribe(new BaseObserver<BaseResponse<GameContainerBean>>(this,false) {
@Override
public void onSuccess(BaseResponse<GameContainerBean> response) {
LogUtils.d(response.realData);
}
});
GameRepository繼承于DBlistRepository需要做的事情很少
public class GameBeanRespository extends DBListRepository<GameContainerBean,GameContainerBean.GameListBean> {
@Override
public Class getTClass() {
return GameContainerBean.class;
}
@Override
public Class getTableClass() {
return GameContainerBean.GameListBean.class;
}
@Override
public List<GameContainerBean.GameListBean> mapContainer(GameContainerBean beanContainer) {
return beanContainer.gameList;
}
@Override
public GameContainerBean mapTableBean(List<GameContainerBean.GameListBean> gameListBeen) {
return new GameContainerBean(gameListBeen);
}
}
上面看到的rxMap只是我寫的一個鏈式調(diào)用的Map包裝類凶朗,鏈式調(diào)用編寫的效率和心情大家應(yīng)該都理解 -3-
有興趣的可以拿去用,也就是個小玩意兒显拳。
public class RxMap<T,R>{
Map<T,R> map;
public static <T,R> RxMap<T,R> newInstance(){
return new RxMap<>();
}
public RxMap() {
this.map = new HashMap<>();
}
public RxMap(Map<T,R> map) {
this.map = map;
}
public RxMap<T,R> put(T t, R r){
map.put(t,r);
return this;
}
public Map<T,R> build(){
return map;
}
}
DBflow
簡單的說下DBflow,可能你直接看到了bean的實例進行了數(shù)據(jù)庫的save操作棚愤,覺得很酸爽,確實很酸爽杂数,而且DBflow繼承了GreenDao和OrmLite各自的優(yōu)點宛畦,簡單易用上無可挑剔,自動生成數(shù)據(jù)Dao類揍移,只需要類似于OrmLite利用注解聲明各個bean之間的關(guān)系次和,另外繼承BaseModel就讓bean自己具備了增刪改的能力了。
關(guān)于DBflow這個數(shù)據(jù)庫的使用我就不多說了那伐,因為太簡單踏施,學(xué)習(xí)成本低石蔗,推薦大家去用,用了感覺不爽來打我 - -3┬巍养距!,當然我不會告訴你我在哪里上班的束亏。
后續(xù)我會抽出一個框架的demo的github地址補充在文章下面铃在。