最喜歡干的事,莫過于拿著工資搭框架了。
其實這個框架已經出來很久了,并不是什么新鮮玩意兒了,只不過我一直沒有嘗試著去寫一篇內容比較大的文章來分享,這次就賣弄一下,希望各種大神輕噴,有什么問題也希望各位大神不吝賜教。
Retrofit的接入
ApiService
首先Retrofit的框架架構搭建其實比較簡單,因為Retrofit本身已經極致簡單了。
/**
* 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);
}
上面的代碼通過將接口返回類型通用化返回結合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進行必要的配置
緩存攔截器
細心的讀者可能發現了CacheNetworkInterceptor這個注釋的攔截器,它的職責本來要添加的NetworkInterceptor是為了做緩存Hook的。
但是查閱了一些資料,還有okhttp源碼,其實okhttp本身是自帶緩存邏輯的,這套邏輯完全遵守RFC協議進行緩存控制的。很多人都去hook掉了這步。其實查閱源碼可以看到
從源碼中不難看出,我們在自定義cache的時候,okhttp會把自己的internalCache給廢棄掉,而我們在okhttp的內部攔截器中也會看到CacheInterceptor,這個類其實就是實現了okhttp的Cache-control。所以我并沒有選擇去攔截Response手動添加Cache-control進行緩存處理。當然大家要用我也攔不住,畢竟也挺方便的。
日志攔截器
LoggingInterceptor攔截器主要是為了打印請求發送和收到請求的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();//請求發起的時間
LogUtils.e(String.format("發送請求 %s on %s%n%s", request.url(), chain.connection(), request.headers()));
Response response = chain.proceed(request);
long t2 = System.nanoTime();//收到響應的時間
//這里不能直接使用response.body().string()的方式輸出日志
//因為response.body().string()之后,response中的流會被關閉,程序會報錯,我們需要創建出一
//個新的response給應用層處理
ResponseBody responseBody = response.peekBody(1024 * 1024);
LogUtils.d(String.format("接收響應: [%s]" +
"\n %n返回json:【%100s】 " +
"\n請求執行時間%.1fms" +
"\n%n%s",
response.request().url(),
responseBody.string(),
(t2 - t1) / 1e6d,
response.headers()));
return response;
}
}
加上日志攔截器之后log如下圖
看到打印出來的詳細的log有木有感覺很酸爽。
Rxjava的封裝
綁定Activity生命周期
對rxjava中的subcriber的封裝,這里主要是將activity的生命周期和subcriber綁定聯系起來,當activity被finish的時候我們的subcriber也應該dispose取消掉。
private CompositeDisposable disposables2Stop;// 管理Stop取消訂閱者者
private CompositeDisposable disposables2Destroy;// 管理Destroy取消訂閱者者
在baseaActivity中通過CompositeDisposable組合管理添加進來的Disposable。然后在ondestroy中進行統一取消,防止內存泄漏。
@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,在錯誤回調中的代碼,前半部分是獲取錯誤的堆棧進行打印的邏輯,后面是對各類錯誤的通用處理。
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中另一個最重要的結果回調onNext中對errcode進行過濾,因為我自己封裝的model層返回的BaseResponce是沒有errorCode的,這個model后面會講到,當然我也可以自己給通用BaseResponse加上200的code但是總感覺這兩個邏輯還是不要耦合的好,萬一code變了我model也要改,所以我在我在BaseResponce中設置了一個變量fromCache用于標記返回結果為緩存。代碼如下:
@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;
/**
* 請求結果是否來自緩存
*/
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 是否在執行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 服務器返回的數據
*/
abstract public void onSuccess(T response);
/**
* 服務器返回數據,但響應碼不為200
*
* @param response 服務器返回的數據
*/
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;
}
}
/**
* 請求網絡失敗原因
*/
public enum ExceptionReason {
/**
* 解析數據失敗
*/
PARSE_ERROR,
/**
* 網絡問題
*/
BAD_NETWORK,
/**
* 連接錯誤
*/
CONNECT_ERROR,
/**
* 連接超時
*/
CONNECT_TIMEOUT,
/**
* 未知錯誤
*/
UNKNOWN_ERROR,
}
}
Model層的封裝
邏輯流程圖
然后說下上面提到的model層,我定義了接口IRepository。
這個model的主要邏輯是 :
首先判斷是否需要強制刷新,如果不需要強制刷新則去數據庫緩存中查看是否含有對象的緩存,如果是網絡獲取判斷是否需要緩存。這里的邏輯主要由客戶端控制。
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的實現
拿目前公司的restful接口數據格式類型舉例:
{
"code":200,
"msg":"請求成功",
"data":{
"count":10,
"game_list":[
{
"gameid":362938
}
]
}
}
可以看出BaseResponce返回的泛型T對應的data數據還需要繼續解析。所以以目前的IRepository<T>
public abstract class IDBFlowRespository<BeanContainer,DBBean> implements IRepository<BaseResponse<BeanContainer>>{
是代碼是不能很好的封裝滿足需求的,所以我定義了抽象類繼承IRepository。
定義了2個泛型BeanContainer和DBBean,數據庫的相關操作基本由DBBean泛型實例完成,網絡層的解析由BeanContainer完成。
各司其職。GameContainer對應的是上圖json的data,gameList對應的是上圖json的game_list。當然如果有其他類型的restful結構,我只需要在定義對應類型的repository抽象類就好了,畢竟現在返回的restful接口的json格式非常局限滿世界也就那么幾種,所以不用擔心repository的擴展類太多的問題。
而真正的Repository實例代碼非常少,只需要繼承4個接口就能滿足上述定義的model接口的功能,如下:
public class GameBeanRespository extends DBListRepository<GameContainerBean,GameContainerBean.GameListBean> {
//用于Gson對泛型的解析
@Override
public Class getTClass() {
return GameContainerBean.class;
}
//用于DB抽象類獲取對數據庫的引用
@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可以進行數據庫的增刪改,很方便。
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對應的數據庫增加的字段主要是用來根據url進行緩存查詢的。
其中key是由Url拼接上queryMap的參數組成,邏輯如下:
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();
}
有個小問題,因為網絡數據獲取是從我們定義的retrofit通用接口中返回,返回的對象是Obserable<ResponseBody>而我們的model接受的參數是Observable<BaseResponce<T>>,等于是承包了GsonConvertFactory的工作,我們把返回的Observer通過rxjava的map轉成我們的Model對應的的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對應的onNext邏輯相吻合了。
測試實例
new GameBeanRespository()
.getEntry(new UrlConstant.Builder(false).shuffix().game().list().build()//參數url
,new RxMap()
.put("page","2")
.put("offset","10")
.build())//參數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只是我寫的一個鏈式調用的Map包裝類,鏈式調用編寫的效率和心情大家應該都理解 -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的實例進行了數據庫的save操作,覺得很酸爽,確實很酸爽,而且DBflow繼承了GreenDao和OrmLite各自的優點,簡單易用上無可挑剔,自動生成數據Dao類,只需要類似于OrmLite利用注解聲明各個bean之間的關系,另外繼承BaseModel就讓bean自己具備了增刪改的能力了。
關于DBflow這個數據庫的使用我就不多說了,因為太簡單,學習成本低,推薦大家去用,用了感覺不爽來打我 - -!!,當然我不會告訴你我在哪里上班的。
后續我會抽出一個框架的demo的github地址補充在文章下面。