編輯:關於Android編程
這篇博客裡面就來實踐下。在上一篇博客裡面說到了OkHttp類似HttpUrlConnection。按這樣說的話,我們在項目中肯定還是要封裝一層。如果嫌封裝麻煩的話,也可以拿來主義,比如使用鴻洋大神的OkHttpUtils,網絡上對它也好評如潮。又或者曾經很火的Volley框架。為什麼說曾經呢?也不是說它用的少了,只能說有更火的框架出來了。是什麼呢?沒錯,就是這篇文章說到的Retrofit框架。既然是新框架,那為什麼前面又說是OkHttp的實踐呢?這裡我們就要理解Retrofit這個框架了。Retrofit這個框架網絡請求層事用的是OkHttp,它同樣是Square開源組合推出的一個框架。在Retrofit2.0以前,還可以選擇HttpUrlConnection或者HttpClient去請求。Retrofit最近推出的2.0版本以後,直接強制用戶使用OkHttp去做網絡請求了。所以可以說Retrofit和OkHttp已經是一對同胞兄弟了。
其實Retrofit還沒有廣泛使用的時候,使用的最多的還是Volley框架的。Retrofit和Volley一樣對HttpURLConnection或者OkHttp進行封裝。然後有一天,你和你的同事說,咱們把Volley改成Retrofit框架吧,你同事就問你,Volley用的好好的,干嘛要換。那我們要怎麼勸服他去使用呢?你就會要說,Volley的原理我們通過一系列封裝成為一個Request對象,然後我們把它添加到RequestQueue裡面,然後通過NetworkDispatcher進行網絡請求,而Retrofit只需要定義一個API。就可以直接返回我們要請求的數據了。當然,它最好是一個RestfulAPI。
網上對RestfulAPI這個概念有很多種理解,說的已經讓我們摸不著頭腦了。怎麼來理解RestfulAPI呢?符合Restful風格的就是RestfulAPI。Restful風格有是什麼鬼?RESTful即Representational State Transfer,可以把它翻譯成(資源)表現層狀態轉換。理解這個名詞就懂了。
資源,服務器給客戶端的文字,圖片,視頻都可以理解為資源。我們一般都是URL這個資源實體去指向資源所在的路徑,當然這個路徑必須是名詞組成的,不能是動詞。比如https://www.google.com.hk/這個網址就可以說是一種資源。 表現層(Representational ),http請求的時候,會有http協議的head部分,post請求的時候還有http body。它描述了請求資源的Content-Type和Content-length等等。這就是一種表示層。又或者我們常使用的json格式也是一種表示層。狀態轉換(State Transfer)在http請求中,GET用來獲取資源,POST用來新建資源(也可以用於更新資源),PUT用來更新資源,DELETE用來刪除資源。都是狀態轉換,而這些狀態轉換又是建立在表現層之上的,http頭部表現層就會描述請求是通過get或者post方式等來請求的。
為什麼難理解呢,主要是Restful只是一種風格,沒有一套完整的標准,所以網絡上各有各的理解。
既然這樣,那麼這裡我們就要先准備下幾個基本的RESTful API。我這裡准備了
一個user表
一個新聞列表(news)表<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxpbWcgYWx0PQ=="這裡寫圖片描述" src="/uploadfile/Collfiles/20160620/2016062009092949.png" title="\" />
3個API(我的本地ip為192.168.1.103:8080)
注冊接口 http://192.168.1.103:8080/GoachWeb/RegisterDataServlet
參數:username、password(POST/Get)
返回:
{
"resultCode": 200,
"responseTime": "2016-06-14 22:38:49",
"data": {
"errorCode": 1,
"userId": 1000000,
"userName": "Goach"
}
}
登錄接口 http://192.168.1.103:8080/GoachWeb/LoginDataServlet
參數:username、password(POST/Get)
返回:
{
"resultCode": 200,
"responseTime": "2016-06-14 22:38:49",
"data": {
"errorCode": 1,
"userId": 1000000,
"userName": "Goach"
}
}
新聞列表接口
參數:userId(POST/Get)
返回
{
"resultCode": 200,
"responseTime": "2016-06-18 22:17:30",
"data": {
"newsItem": [
{
"id": 1,
"title": "高盛:中國房地產可能在6-9個月內迎來“拐點",
"content": "6月14日,王逸等高盛分析師在報告中寫道,預計2017年房價將疲弱,因為該行業因槓桿率上升、需求減弱,不久將見到拐點。"
},
{
"id": 2,
"title": "國產大飛機C919首飛時間曝光 已接517架次訂單",
"content": "《經濟參考報》記者日前從多個權威渠道獲悉,我國自主研制的C919大型客機將於今年下半年首飛,最快2017年完成後續各項技術驗證,並開始正式交付。"
},
{
"id": 3,
"title": "解放軍大批巨炮同時開火 現場升碩大火球",
"content": " 6月10日,陸軍第42集團軍某防空旅全員全裝在粵東某陌生地域展開戰場機動、偵察預警、陸空對抗、實彈射擊等課目訓練,錘煉部隊實戰本領。"
},
{
"id": 4,
"title": "拳王鄒市明,一場比賽460萬獎金,只開90萬的車",
"content": "中國拳王鄒市明,一年的收入有多少?和帕奎奧,梅威瑟這種級別的相比,鄒市明的收入只能算是小收入,從最初打職業比賽時的30萬美金的獎金,到最高70萬美金獎金,這其中受了多少傷只有他自己最清楚。如果能7場比賽速成世界拳王,獎金不過也就100萬美金,或許他”永遠“也不能成為梅威瑟這樣的拳王。"
},
{
"id": 5,
"title": "40萬人看楊毅直播講道理???",
"content": " 由總決賽第四場比賽中,楊毅對於詹姆斯和格林的一次沖突而進行的評述,引發的一系列事件,還在持續發酵中。"
},
{
"id": 11,
"title": "女王杯穆雷雙搶7險勝 瓦林卡爆冷止步首輪",
"content": "騰訊體育6月15日訊 ATP500賽倫敦女王杯草地公開賽今日繼續男單首輪比賽的爭奪,賽會頭號種子、英國名將穆雷通過兩盤搶7以7-6(8)和7-6(1)險勝法國選手馬胡特,驚險晉級次輪;而2號種子瑞士名將瓦林卡則連丟兩盤以2-6和6-7(3)不敵西班牙選手沃達斯科,爆冷止步首輪。"
}
]
}
}
接口比較簡單。主要是自己後台開發比較low。這裡後台使用的是通過servlet和jdbc通過gson轉換為json進行開發的。
接口准備好了。下面就來集成Retrofit框架。
添加幾個權限
build.gradle添加依賴,下面會用到的也在這裡了:
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'com.squareup.retrofit2:retrofit:2.0.2'
compile 'com.squareup.retrofit2:converter-gson:2.0.2'
compile 'com.squareup.okhttp3:okhttp:3.3.0'
compile 'com.squareup.okio:okio:1.7.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.0'
compile 'com.android.support:recyclerview-v7:23.4.0'
兩個retrofit依賴包,兩個okhttp依賴包,okhttp3:logging-interceptor依賴包主要是攔截請求日志使用,引入下面要使用的rxandroid的兩個依賴包reactivex:rxandroid和reactivex:rxjava,recyclerview主要是新聞列表頁要使用的。
下面就是寫登錄注冊頁面。
登錄頁面效果如下
注冊頁面效果如下
新聞頁面布局效果
布局代碼後面源碼提供下載,而且比較簡單。
頁面寫好了。下面我們通過單例形式創建一個Retrofit對象。
public class HRetrofitNetHelper{
public static HRetrofitNetHelper mInstance;
public Retrofit mRetrofit;
//本地ip為192.168.1.103
public static final String BASE_URL = "http://192.168.1.103:8080/GoachWeb/";
private HRetrofitNetHelper(){
mRetrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.build();
}
public static HRetrofitNetHelper getInstance(){
if(mInstance==null){
synchronized (HRetrofitNetHelper.class){
if(mInstance==null)
mInstance = new HRetrofitNetHelper ();
}
}
return mInstance ;
}
}
簡單的創建好了一個Retrofit。這裡只是配置了一個接口的baseUrl,也就是根路徑。
如果要Retrofit直接將json轉換為為Dao對象。那麼我們就要通過addConverterFactory來配置,如下:
mRetrofit = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.build();
上面是使用依賴:
compile'com.squareup.retrofit2:converter-gson:2.0.2'
包。然後addConverterFactory來配置。通過源碼方法
addConverterFactory(Converter.Factory factory)
我們可以看到要傳入一個繼承Converter.Factory的對象。Retrofit裡面就有這樣的對象,這裡我們用的是Gson來進行解析,那就有對應的GsonConverterFactory。那好下面就來創建這個對象
創建這個對象有兩種方式
一種是像上面寫的一樣
GsonConverterFactory.create()
這種方式就是簡單的創建默認的Gson對象,然後像我們平常一樣轉換為Dao對象。
還有一種方式就是通過GsonBuilder創建Gson對象,比如這裡統一把後台提供的帶有yyyy-MM-dd HH:mm:ss格式的Date對象,客戶端如果用上面這種方式創建的話,會報下面這個錯
java.text.ParseException: Failed to parse date ["2016-06-11 20:57:28']: Invalid time zone indicator ' ' (at offset 0)
這種情況下,我們就可以這樣:
Gson mGson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss").create();
然後再創建GsonConverterFactory對象的時候傳入Gson
.addConverterFactory(GsonConverterFactory.create(mGson))
就可以很好的解決這個問題了。
這裡只是說了使用Gson進行解析,其實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
用法類似這樣
導入包(xx可以指Jackson或者Moshi等等):
compile 'com.squareup.retrofit2:converter-xx:2.0.2'
然後:
.addConverterFactory(xxConverterFactory.create(mGson))
當然,我們還是可以設置多個converter
比如支持 proto 格式和json格式。那麼如下添加:
Retrofit retrofit = new Retrofit.Builder()
//...
.addConverterFactory(ProtoConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build();
ProtoConverterFactory和GsonConverterFactory添加 converter 的順序很重要。Retrofit會依次詢問每一個 converter 能否處理一個類型。當Retrofit試圖反序列化一個 proto 格式,它其實會被當做 JSON 來對待。所以Retrofit會先要檢查 proto buffer 格式,然後才是 JSON。所以要先添加ProtoConverterFactory,然後是GsonConverterFactory。
又比如我們需要Retrofit支持RxJava。添加:
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
就好了。
Retrofit還可以添加OkHttpClient對象。比如我們可以添加一個攔截器來監聽每次請求體:
依賴的包
compile'com.squareup.okhttp3:logging-interceptor:3.2.0'
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
Log.d("zgx", "OkHttp====message " + message);
}
});
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
創建好後,然後通過retrofit對象添加client,如下:
mRetrofit = new Retrofit.Builder()
//...
.client(mOkHttpClient)
.build();
這樣我們就通過HttpLoggingInterceptor 攔截器可以獲取道http請求體,可以獲取我們請求方式,請求的參數,然後的json數據。這裡以登錄接口為例,如下:
06-11 22:16:11.064 31186-8789/com.goach.client D/zgx: OkHttp====message --> POST http://192.168.1.102:8080/GoachWeb/LoginDataServlet http/1.1
06-11 22:16:11.064 31186-8789/com.goach.client D/zgx: OkHttp====message Content-Type: application/x-www-form-urlencoded
06-11 22:16:11.068 31186-8789/com.goach.client D/zgx: OkHttp====message Content-Length: 30
06-11 22:16:11.068 31186-8789/com.goach.client D/zgx: OkHttp====message
06-11 22:16:11.068 31186-8789/com.goach.client D/zgx: OkHttp====message username=Goach&password=123456
06-11 22:16:11.068 31186-8789/com.goach.client D/zgx: OkHttp====message --> END POST (30-byte body)
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message <-- 200 OK http://192.168.1.102:8080/GoachWeb/LoginDataServlet (1308ms)
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message Server: Apache-Coyote/1.1
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message Content-Type: text/plain;charset=UTF-8
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message Transfer-Encoding: chunked
06-11 22:16:12.376 31186-8789/com.goach.client D/zgx: OkHttp====message Date: Sat, 11 Jun 2016 14:15:19 GMT
06-11 22:16:12.384 31186-8789/com.goach.client D/zgx: OkHttp====message
06-11 22:16:12.384 31186-8789/com.goach.client D/zgx: OkHttp====message {"errorCode":1,"userId":1000000,"responseTime":"2016-06-11 22:15:19","resultCode":200}
06-11 22:16:12.384 31186-8789/com.goach.client D/zgx: OkHttp====message <-- END HTTP (86-byte body)
既然能用OkHttp的攔截機制,那麼我們就可以在RequestBody 裡面添加基本參數
我們可以再新建一個攔截器,這裡我舉例加些簡單的系統參數,如下:
class HttpBaseParamsLoggingInterceptor implements Interceptor{
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder requestBuilder = request.newBuilder();
RequestBody formBody = new FormBody.Builder()
.add("userId", "10000")
.add("sessionToken", "E34343RDFDRGRT43RFERGFRE")
.add("q_version", "1.1")
.add("device_id", "android-344365")
.add("device_os", "android")
.add("device_osversion","6.0")
.add("req_timestamp", System.currentTimeMillis() + "")
.add("app_name","forums")
.add("sign", "md5")
.build();
String postBodyString = Utils.bodyToString(request.body());
postBodyString += ((postBodyString.length() > 0) ? "&" : "") + Utils.bodyToString(formBody);
request = requestBuilder
.post(RequestBody.create(MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8"),
postBodyString))
.build();
return chain.proceed(request);
}
}
上面Utils類是使用的okio.Buffer裡面的工具類。通過RequestBody構建要上傳的一些基本公共的參數,然後通過”&”符號在http 的body裡面其他要提交參數拼接。然後再通過requestBuilder重新創建request對象,然後再通過chain.proceed(request)返回Response 。
接下來在創建OkHttpClient對象的時候修改為如下代碼:
mOkHttpClient = new OkHttpClient.Builder()
.addInterceptor(interceptor)
.addInterceptor(new HttpBaseParamsLoggingInterceptor())
.build();
這樣就添加好了一些基本的公共參數。
當然。我們也可以直接借助github 上的BasicParamsInterceptor。代碼如下:
public class BasicParamsInterceptor implements Interceptor {
Map queryParamsMap = new HashMap<>();
Map paramsMap = new HashMap<>();
Map headerParamsMap = new HashMap<>();
List headerLinesList = new ArrayList<>();
private BasicParamsInterceptor() {
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder requestBuilder = request.newBuilder();
// process header params inject
Headers.Builder headerBuilder = request.headers().newBuilder();
if (headerParamsMap.size() > 0) {
Iterator iterator = headerParamsMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry entry = (Map.Entry) iterator.next();
headerBuilder.add((String) entry.getKey(), (String) entry.getValue());
}
}
if (headerLinesList.size() > 0) {
for (String line: headerLinesList) {
headerBuilder.add(line);
}
}
requestBuilder.headers(headerBuilder.build());
// process header params end
// process queryParams inject whatever it's GET or POST
if (queryParamsMap.size() > 0) {
injectParamsIntoUrl(request, requestBuilder, queryParamsMap);
}
// process header params end
// process post body inject
if (request.method().equals("POST") && request.body().contentType().subtype().equals("x-www-form-urlencoded")) {
FormBody.Builder formBodyBuilder = new FormBody.Builder();
if (paramsMap.size() > 0) {
Iterator iterator = paramsMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry entry = (Map.Entry) iterator.next();
formBodyBuilder.add((String) entry.getKey(), (String) entry.getValue());
}
}
RequestBody formBody = formBodyBuilder.build();
String postBodyString = bodyToString(request.body());
postBodyString += ((postBodyString.length() > 0) ? "&" : "") + bodyToString(formBody);
requestBuilder.post(RequestBody.create(MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8"), postBodyString));
} else { // can't inject into body, then inject into url
injectParamsIntoUrl(request, requestBuilder, paramsMap);
}
request = requestBuilder.build();
return chain.proceed(request);
}
// func to inject params into url
private void injectParamsIntoUrl(Request request, Request.Builder requestBuilder, Map paramsMap) {
HttpUrl.Builder httpUrlBuilder = request.url().newBuilder();
if (paramsMap.size() > 0) {
Iterator iterator = paramsMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry entry = (Map.Entry) iterator.next();
httpUrlBuilder.addQueryParameter((String) entry.getKey(), (String) entry.getValue());
}
}
requestBuilder.url(httpUrlBuilder.build());
}
private static String bodyToString(final RequestBody request){
try {
final RequestBody copy = request;
final Buffer buffer = new Buffer();
if(copy != null)
copy.writeTo(buffer);
else
return "";
return buffer.readUtf8();
}
catch (final IOException e) {
return "did not work";
}
}
public static class Builder {
BasicParamsInterceptor interceptor;
public Builder() {
interceptor = new BasicParamsInterceptor();
}
public Builder addParam(String key, String value) {
interceptor.paramsMap.put(key, value);
return this;
}
public Builder addParamsMap(Map paramsMap) {
interceptor.paramsMap.putAll(paramsMap);
return this;
}
public Builder addHeaderParam(String key, String value) {
interceptor.headerParamsMap.put(key, value);
return this;
}
public Builder addHeaderParamsMap(Map headerParamsMap) {
interceptor.headerParamsMap.putAll(headerParamsMap);
return this;
}
public Builder addHeaderLine(String headerLine) {
int index = headerLine.indexOf(":");
if (index == -1) {
throw new IllegalArgumentException("Unexpected header: " + headerLine);
}
interceptor.headerLinesList.add(headerLine);
return this;
}
public Builder addHeaderLinesList(List headerLinesList) {
for (String headerLine: headerLinesList) {
int index = headerLine.indexOf(":");
if (index == -1) {
throw new IllegalArgumentException("Unexpected header: " + headerLine);
}
interceptor.headerLinesList.add(headerLine);
}
return this;
}
public Builder addQueryParam(String key, String value) {
interceptor.queryParamsMap.put(key, value);
return this;
}
public Builder addQueryParamsMap(Map queryParamsMap) {
interceptor.queryParamsMap.putAll(queryParamsMap);
return this;
}
public BasicParamsInterceptor build() {
return interceptor;
}
}
}
我們只要向上面一樣配置就行了。
其實攔截器還能做很多事。比如在開發中,我們會遇到,我們去請求某些接口的時候,服務端會直接返回一個信息給客戶端,讓客戶端去Toast提示。下面,我就以只要是請求登錄接口就給個提示框為例
還是會用到攔截器,要知道,攔截器接口實現的intercept這個方法可不是在ui線程裡面執行的,所以這裡彈Toast,我們用RxAndroid實現再好不過了。
既然要用到RxAndroid,那就需要再依賴兩個包:
compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.0'
依賴好了,下面就可以在OkHttpClient創建的時候再添加一個攔截器mUrlInterceptor,代碼如下,
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
okhttp3.Response response = chain.proceed(request);
String requestUrl = response.request().url().uri().getPath();
if(!TextUtils.isEmpty(requestUrl)){
if(requestUrl.contains("LoginDataServlet")) {
if (Looper.myLooper() == null) {
Looper.prepare();
}
createObservable("現在請求的是登錄接口");
}
}
return response;
}
然後再上面OkHttp創建的時候修改下:
mOkHttpClient = new OkHttpClient.Builder()
//..前面兩個攔截器省略
.addInterceptor(mUrlInterceptor)
.build();
說下上面intercept裡面的,注意在createObservable方法調用前,要先Looper.prepare()下,否則會報錯提示你要先調用Looper.prepare()方法下。其他的代碼應該理解沒什麼問題了。我們知道RxAndroid兩個核心就是Observable事件被觀察者,然後就是subscribe事件訂閱者,可以理解為觀察者模式,但是它和觀察者模式又有不同的地方,就是當事件被觀察者沒有關注者的時候,事件不會發送出去。詳細就不講解了。我這裡只是彈個Toast,不用那麼復雜。代碼如下:
private void createObservable(String msg){
Observable.just(msg).map(new Func1() {
@Override
public String call(String s) {
return s;
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(onNextAction);
}
通過Func1,直接發送一條消息給訂閱者,發送完後這個事件就結束了。
.observeOn(AndroidSchedulers.mainThread())
的作用就是把訂閱者處理事件發送給ui線程去處理。
接下來訂閱者,就簡單的用onNextAction實現了。
private void createSubscriberByAction() {
onNextAction = new Action1() {
@Override
public void call(String s) {
Log.d("zgx","s=========="+s);
Toast.makeText(mContext,s, Toast.LENGTH_SHORT).show();
}
};
}
createSubscriberByAction方法在HRetrofitNetHelper對象構造器裡面調用就好了。
private HRetrofitNetHelper(Context context){
//...
createSubscriberByAction();
//...
}
這樣就實現了,上面提的需求。
配置了這麼多,接下來肯定又會想到緩存問題還沒有處理呢。那麼,接下來就來說下緩存處理了。
寫之前,先看下源碼裡面注釋的一段話
if (!requestMethod.equals("GET")) {
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
return null;
}
看懂了吧,OkHttp建議在不是Get請求的響應體不要緩存,因為如果緩存的話會提高它的復雜性而且好處不大。
沒看到這段話之前。郁悶了很久為什麼Post請求緩存生成不了,而且會報一個錯
504 Unsatisfiable Request (only-if-cached)
這個錯的意思就是只去讀緩存,但是緩存不存在,所以就會報錯了。但是我覺得有時候Post請求緩存的需求還是會有的,比如有時候在應用中經常想在沒網的情況下緩存這個頁面,而這個頁面的請求接口也是post請求。所以還是要有緩存更好,比如volley框架就可以緩存整個頁面,但是也是要改下volley的代碼。目前還不知道怎麼去緩存post請求。目前github上有RxCache,或者是通過Sqlite自己實現緩存都有,沒有仔細研究,後面有時間在看。
下面就來看下實現代碼
private final Cache cache;
public Cache getCache(){
return cache;
}
public void clearCache() throws IOException {
cache.delete();
}
配置OkHttp緩存
File cacheFile = new File(context.getCacheDir(), "HttpCache");
cache = new Cache(cacheFile, 1024 * 1024 * 100); //100Mb
mOkHttpClient = new OkHttpClient.Builder()
//...
.cache(cache)
.build();
官方建議緩存路徑寫在context.getCacheDir()裡面,也就是在/data/data/com.goach.client/cache/HttpCache裡面。這樣配置好了,如果雲端通過http的header裡面Cache-Control做了緩存。那麼這樣就緩存完了。但是如果雲端沒有做了,那麼我們客戶端也可以自己通過Interceptor實現。這裡我就把緩存邏輯寫在上面的mUrlInterceptor攔截器裡面了。修改如下
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
//緩存
if(NetUtil.checkNetwork(mContext)==NetUtil.NO_NETWORK){
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
Log.d("zgx","no network");
}
okhttp3.Response response = chain.proceed(request);
String requestUrl = response.request().url().uri().getPath();
if(!TextUtils.isEmpty(requestUrl)){
if(requestUrl.contains("LoginDataServlet")) {
if (Looper.myLooper() == null) {
Looper.prepare();
}
createObservable("現在請求的是登錄接口");
}
}
//緩存響應
if(NetUtil.checkNetwork(mContext)!=NetUtil.NO_NETWORK){
//有網的時候讀接口上的@Headers裡的配置,你可以在這裡進行統一的設置
String cacheControl = request.cacheControl().toString();
Log.d("zgx","cacheControl====="+cacheControl);
return response.newBuilder()
.header("Cache-Control", cacheControl)
//http1.0的舊東西,優先級比Cache-Control低
.removeHeader("Pragma")
.build();
}else{
return response.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=30*24*60*60")
.removeHeader("Pragma")
.build();
}
}
沒網的情況下Request 直接從緩存裡面讀取,響應體增加header的Cache-Control,緩存30天,有網的情況下,Request 就會去請求服務器,然後響應體就會去都Retrofit框架裡面的@Header配置,如果沒有配置,就沒不緩存,如果配置了就可以進行緩存。到這裡,當我們去Get請求的時候,就會生成緩存
我這裡是通過模擬器看到,真機裡面是看不到的。打開可以看到我們請求信息。
okhttp如果沒有配置默認是10s,錯誤信息如下
onFailure======java.net.SocketTimeoutException: failed to connect to /192.168.1.101 (port 8080) after 10000ms
配置
mOkHttpClient = new OkHttpClient.Builder()
.connectTimeout(12, TimeUnit.SECONDS)
//...
.build();
後,錯誤信息如下
onFailure======java.net.SocketTimeoutException: failed to connect to /192.168.1.101 (port 8080) after 12000ms
還可以配置
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
沒毛病,應該看的懂。
這樣Retrofit創建基本的配置就完成了,最後結合上面總結後整個配置類的代碼:
public class HRetrofitNetHelper implements HttpLoggingInterceptor.Logger,Interceptor {
//HRetrofitNetHelper 實現單例
public static HRetrofitNetHelper mInstance;
//緩存對象
private final Cache cache;
public Retrofit mRetrofit;
public OkHttpClient mOkHttpClient;
//請求日志攔截器
public HttpLoggingInterceptor mHttpLogInterceptor;
//基本參數攔截器
private BasicParamsInterceptor mBaseParamsInterceptor;
//緩存和特殊Url攔截處理攔截器
private Interceptor mUrlInterceptor;
private Context mContext;
//Date對象傳遞
public Gson mGson;
//接口baseurl
public static final String BASE_URL = "http://192.168.1.101:8080/GoachWeb/";
private Action1 onNextAction;
private HRetrofitNetHelper(Context context){
this.mContext = context ;
//提供Action,供特殊Url攔截然後Toast
createSubscriberByAction();
//yyyy-MM-dd HH:mm:ss的時間格式,可以轉換為Date對象
mGson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss")
.create();
mHttpLogInterceptor = new HttpLoggingInterceptor(this);
//打印http的body體
mHttpLogInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
//基本參數
Map tempParams = getBaseParams(context);
mBaseParamsInterceptor = new BasicParamsInterceptor.Builder()
.addParamsMap(tempParams)
.build();
mUrlInterceptor = this;
//創建緩存路徑
File cacheFile = new File(context.getCacheDir(), "HttpCache");
Log.d("zgx","cacheFile====="+cacheFile.getAbsolutePath());
cache = new Cache(cacheFile, 1024 * 1024 * 100); //100Mb
mOkHttpClient = new OkHttpClient.Builder()
.connectTimeout(12, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(mHttpLogInterceptor)
.addInterceptor(mBaseParamsInterceptor)
.addInterceptor(mUrlInterceptor)
.cache(cache)
.build();
mRetrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(mGson))
.client(mOkHttpClient)
.build();
}
public static HRetrofitNetHelper getInstance(Context context){
if(mInstance==null){
synchronized (HRetrofitNetHelper.class){
if(mInstance==null){
mInstance = new HRetrofitNetHelper(context);
}
}
}
return mInstance;
}
//獲取相應的APIService對象
public T getAPIService(Class service) {
return mRetrofit.create(service);
}
//異步callback,對一些特殊response邏輯處理
public void enqueueCall(Call> call,final RetrofitCallBack retrofitCallBack){
call.enqueue(new Callback>() {
@Override
public void onResponse(Call> call, Response> response) {
BaseResp resp = response.body() ;
if (resp == null) {
Toast.makeText(mContext, "暫時沒有最新數據!", Toast.LENGTH_SHORT).show();
return;
}
if (resp.getResultCode() == 2000 || resp.getResultCode() == 2001 || resp.getResultCode() == 2002) {
Toast.makeText(mContext,"code====="+resp.getResultCode(),Toast.LENGTH_SHORT).show();
}
if (resp.getResultCode() == 200) {
if(retrofitCallBack!=null)
retrofitCallBack.onSuccess(resp);
} else {
// ToastMaker.makeToast(mContext, resp.errMsg, Toast.LENGTH_SHORT);
if(retrofitCallBack!=null)
retrofitCallBack.onFailure(resp.getErrMsg());
}
}
@Override
public void onFailure(Call> call, Throwable t) {
// ToastMaker.makeToast(mContext, "網絡錯誤,請重試!", Toast.LENGTH_SHORT);
if(retrofitCallBack!=null){
retrofitCallBack.onFailure(t.toString());
}
}
});
}
@Override
public void log(String message) {
Log.d("zgx","OkHttp: " + message);
}
//提供一些常用的基本參數
public Map getBaseParams(Context context){
Map params = new HashMap<>();
params.put("userId", "324353");
params.put("sessionToken", "434334");
params.put("q_version", "1.1");
params.put("device_id", "android7.0");
params.put("device_os", "android");
params.put("device_type", "android");
params.put("device_osversion", "android");
params.put("req_timestamp", System.currentTimeMillis() + "");
params.put("app_name","forums");
String sign = makeSign(params);
params.put("sign", sign);
return params ;
}
public String makeSign(Map params) {
final String signSalt = "fe#%d8ec93a1159a2a3";
TreeMap sorted = new TreeMap();
for (Map.Entry kv : params.entrySet()) {
sorted.put(kv.getKey(), kv.getValue());
}
StringBuilder sb = new StringBuilder(signSalt);
for (String key : sorted.keySet()) {
if (!"sign".equals(key) && !key.startsWith("file_")) {
sb.append(key).append(sorted.get(key));
}
}
sb.append(signSalt);
return MD5.md5(sb.toString()).toUpperCase();
}
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
//緩存
if(NetUtil.checkNetwork(mContext)==NetUtil.NO_NETWORK){
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
Log.d("zgx","no network");
}
okhttp3.Response response = chain.proceed(request);
String requestUrl = response.request().url().uri().getPath();
if(!TextUtils.isEmpty(requestUrl)){
if(requestUrl.contains("LoginDataServlet")) {
if (Looper.myLooper() == null) {
Looper.prepare();
}
createObservable("現在請求的是登錄接口");
}
}
//緩存響應
if(NetUtil.checkNetwork(mContext)!=NetUtil.NO_NETWORK){
//有網的時候讀接口上的@Headers裡的配置,你可以在這裡進行統一的設置
String cacheControl = request.cacheControl().toString();
Log.d("zgx","cacheControl====="+cacheControl);
return response.newBuilder()
.header("Cache-Control", cacheControl)
.removeHeader("Pragma")
.build();
}else{
return response.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=120")
.removeHeader("Pragma")
.build();
}
}
//異步特殊處理後回調
public interface RetrofitCallBack{
void onSuccess(BaseResp baseResp);
void onFailure(String error);
}
private void createSubscriberByAction() {
onNextAction = new Action1() {
@Override
public void call(String s) {
Log.d("zgx","s=========="+s);
Toast.makeText(mContext,s, Toast.LENGTH_SHORT).show();
}
};
}
//創建事件源
private void createObservable(String msg){
Observable.just(msg).map(new Func1() {
@Override
public String call(String s) {
return s;
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(onNextAction);
}
public Cache getCache(){
return cache;
}
public void clearCache() throws IOException {
cache.delete();
}
}
定義上面的3個請求接口API,為了驗證緩存,都是Get請求。
public interface ILoginService {
@GET("LoginDataServlet")
@Headers("Cache-Control: public, max-age=30")
Call> userLogin(@Query("username") String username, @Query("password") String password);
}
public interface INewsService {
@GET("NewsDataServlet")
@Headers("Cache-Control: public, max-age=30")
Call> userNews(@Query("userId") String userId);
}
public interface IRegisterService {
@FormUrlEncoded
@POST("RegisterDataServlet")
Call createUser(@FieldMap Map params);
}
其中Get請求,使用@GET和@Query或者@QueryMap的結合,Post請求@FormUrlEncoded、@POST和@Field或者@FieldMap的結合。又或者url中通過@Path動態添加參數。比如
public interface INewsService
{
@GET("NewsDataServlet/currentPage={currentPage}")
Call> getUser(@Path("currentPage") String currentPage);
}
還有通過@Multipart 實現文件上傳等等,詳細可以看鴻洋大神的 Retrofit2 完全解析 探索與okhttp之間的關系
BaseActivity
public abstract class BaseActivity extends AppCompatActivity{
public HRetrofitNetHelper retrofitNetHelper;
public LayoutInflater mInflater;
public ProgressDialog mDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void setContentView(@LayoutRes int layoutResID) {
super.setContentView(layoutResID);
mInflater = LayoutInflater.from(this);
setContentView(mInflater.inflate(layoutResID,null));
}
@Override
public void setContentView(View view) {
super.setContentView(view);
retrofitNetHelper = HRetrofitNetHelper.getInstance(BaseActivity.this);
mDialog = new ProgressDialog(BaseActivity.this);
}
}
LonigActicity
public class LoginActivity extends BaseActivity implements View.OnClickListener,HRetrofitNetHelper.RetrofitCallBack {
private AutoCompleteTextView mEmailView;
private EditText mPasswordView;
private View mLoginFormView;
private Button mSignInButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
mEmailView = (AutoCompleteTextView) findViewById(R.id.email);
mPasswordView = (EditText) findViewById(R.id.password);
mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) {
if (id == R.id.login || id == EditorInfo.IME_NULL) {
return true;
}
return false;
}
});
mSignInButton = (Button) findViewById(R.id.sign_in_button);
mSignInButton.setOnClickListener(this);
mLoginFormView = findViewById(R.id.login_form);
}
public void startRegister(View view){
Intent intent = new Intent(LoginActivity.this,RegisterActivity.class);
startActivity(intent);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.sign_in_button:
mDialog.setMessage("正在登錄中,請稍後...");
mDialog.show();
ILoginService loginService = retrofitNetHelper.getAPIService(ILoginService.class);
String username = mEmailView.getText().toString();
String password = mPasswordView.getText().toString();
if(!TextUtils.isEmpty(username)&&!TextUtils.isEmpty(password)){
final Call> repos = loginService.userLogin(username,password);
retrofitNetHelper.enqueueCall(repos,this);
}
break;
}
}
@Override
public void onSuccess(BaseResp baseResp) {
Log.d("zgx","onResponse======"+baseResp.getData().getErrorCode());
Date date = baseResp.getResponseTime();
Log.d("zgx","RegisterBean======"+date);
if(baseResp.getData().getErrorCode()==1){
Intent intent = new Intent(LoginActivity.this, NewsActivity.class);
intent.putExtra("intent_user_id",String.valueOf(baseResp.getData().getUserId()));
startActivity(intent);
Toast.makeText(getBaseContext(),"登錄成功",Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(getBaseContext(),"用戶不存在",Toast.LENGTH_SHORT).show();
}
mDialog.dismiss();
}
@Override
public void onFailure(String error) {
Log.d("zgx","onFailure======"+error);
mDialog.dismiss();
}
}
RegisterActivity
public class RegisterActivity extends BaseActivity implements Callback {
private AutoCompleteTextView mUserName;
private EditText mPasswordEditText;
private EditText mConfirmationEditText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_register);
mUserName = (AutoCompleteTextView) findViewById(R.id.id_username);
mPasswordEditText = (EditText)findViewById(R.id.password);
mConfirmationEditText = (EditText)findViewById(R.id.confirmation_password);
}
public void startRegister(View view){
String userName = mUserName.getText().toString();
String password = mPasswordEditText.getText().toString();
String mConfirmation = mConfirmationEditText.getText().toString();
if(!TextUtils.isEmpty(userName)&&!TextUtils.isEmpty(password)
&&!TextUtils.isEmpty(mConfirmation)){
if(password.equals(mConfirmation)){
IRegisterService loginService = retrofitNetHelper.getAPIService(IRegisterService.class);
Map mParamsMap = new HashMap<>();
mParamsMap.put("username",userName);
mParamsMap.put("password",password);
Call call = loginService.createUser(mParamsMap);
call.enqueue(this);
}else {
Toast.makeText(getBaseContext(),"密碼不一致",Toast.LENGTH_SHORT).show();
}
}else {
Toast.makeText(getBaseContext(),"請填寫完整",Toast.LENGTH_SHORT).show();
}
}
@Override
public void onResponse(Call call, Response response) {
if(response.body().getErrorCode()==1){
Intent intent = new Intent(RegisterActivity.this, LoginActivity.class);
startActivity(intent);
}else{
Toast.makeText(getBaseContext(),"注冊失敗",Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call call, Throwable t) {
}
}
NewsActivity
public class NewsActivity extends BaseActivity implements HRetrofitNetHelper.RetrofitCallBack{
private String mUserId;
private RecyclerView mRecyclerView;
private NewsAdapter mNewsAdapter;
private List mDataList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_news);
mUserId = getIntent().getStringExtra("intent_user_id");
mDataList = new ArrayList<>();
mRecyclerView = (RecyclerView)findViewById(R.id.id_news_recycler_view);
LinearLayoutManager manager = new LinearLayoutManager(NewsActivity.this);
mRecyclerView.setLayoutManager(manager);
mNewsAdapter = new NewsAdapter(NewsActivity.this,mDataList);
mRecyclerView.setAdapter(mNewsAdapter);
loadData();
}
private void loadData(){
mDialog.setMessage("正在加載中,請稍後...");
mDialog.show();
INewsService newService = retrofitNetHelper.getAPIService(INewsService.class);
Log.d("zgx","mUserId====="+mUserId);
final Call> repos = newService.userNews(mUserId);
retrofitNetHelper.enqueueCall(repos,this);
}
@Override
public void onSuccess(BaseResp baseResp) {
mDialog.dismiss();
mDataList.clear();
mDataList.addAll(baseResp.getData().getNewsItem());
mNewsAdapter.notifyDataSetChanged();
}
@Override
public void onFailure(String error) {
mDialog.dismiss();
Toast.makeText(NewsActivity.this,"請求出現異常"+error,Toast.LENGTH_SHORT).show();
}
}
其他一些幫助類,後面提供源碼下載。
最後來看下實現的效果
緩存效果沒有錄制,博客上傳文件有限。
只是提供服務端和客戶端的源碼,數據庫表和環境搭建配置就不提供了。
使用的環境為:
Android studio 2.1.2
MyEclipse 2014GA
Tomcat8.0
JDK8.0
MySQL Server 5.7
Navicat for MySQL
為什麼我說它是最實用的 ViewPager 指示器控件呢? 它有以下幾個特點: 1、通過自定義 View 來實現,代碼簡單易懂; 2、使用起來非常方便; 3、通用性高,大
1.前言: 自己也是參考別人的一些自定義view例子,學習了一些基本的自定義view的方法。今天,我參考了一些資料,再結合自已的一些理解,做了一個一鍵清除的動畫。當年,我
WebView組件本身就是一個浏覽器實現,Android5.0增強的WebView基於Chromium M37,直接支持WebRTC、WebAudio、WebGL。開發者
項目效果如下:項目目錄結構如下:代碼如下:AudioManager.javapackage com.xuliugen.weichat;import java.io.Fil