Retrofit2的簡單應用與封裝
我用Android Studio開發,首先,在build.gradle中引入Retrofit的依賴
compile 'com.squareup.retrofit2:retrofit:2.0.0'
這是寫這篇部落格時Retrofit的最新版本,2.0版本跟之前的1.*版本還是有不少不同的地方,如果是從1.*版本切換到2.0,還是有不少地方需要進行修改。
先介紹下最基本的Get請求,訪問一個url地址,返回引數,客戶端接收並打印出來:
(1)定義一個介面,封裝訪問的方法
public interface RetrofitService { @GET("getMethod") Call<String> getTest(); }
首先,可以看到註解@GET,表示這是一個GET請求,裡面的getMethod是GET請求的名稱,也就是URL中請求部分的地址,接下來,getTest是請求的方法名稱,返回的是一個Call物件,泛型為String,注意,從Retrofit2開始,網路請求就是用的OKHttp,而之前的1.*版本只有在引入了OKHttp的前提下才會使用,否則會跟Volley一樣,2.3之前使用HttpClient,2.3之後使用HttpURLConnection,OKHttp裡返回的是Call物件,所以這裡也是返回Call,具體細節大家可以看原始碼,這裡不影響理解,只要知道Retrofit的所有返回都是一個Call物件即可。
(2)定義一個基類用來生成(1)中定義的介面
baseUrl是網路請求的基礎地址,192.168.0.105是我本機的IP,8082是伺服器埠號,我是使用的湯姆貓,其它也可以,看個人習慣,MyWeb是新建的一個Web專案,用來接收客戶端請求。接下來,生成一個Retrofit物件,這裡主要指定2個引數,一個就是網路請求的基礎地址,另一個就是轉換工廠,注意,1.*版本預設使用Gson,而2.0版本則必須明確指定解析服務端相應的轉換方式,我現在服務端返回給客戶端的就是一個簡單的字串,所以我選用Scalars,具體轉換方式有幾種:public class RetroFactory { private static String baseUrl = "http://192.168.0.105:8082/MyWeb/"; private static Retrofit stringRetrofit = new Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(ScalarsConverterFactory.create()) .build(); public static RetrofitService getStringService() { RetrofitService service = stringRetrofit.create(RetrofitService.class); return service; } }
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
使用者需要根據伺服器實際返回的型別做出選擇,這裡選用的最後一種,當然,別忘了在build.gradle中新增依賴compile 'com.squareup.retrofit2:converter-scalars:2.0.0'。建立了Retrofit物件後,使用create方法,即可生成(1)中定義的介面
(3) 定義Servlet
Servlet是服務端用來接收客戶端請求的地方,這裡很簡單,直接接收並返回一個字串
@WebServlet(name="getMethod",value="/getMethod")
public class GetServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.getWriter().write("haha");
}
}
從Servlet3.0開始,不再需要web.xml來定義Servlet,直接通過註解的方式,注意這裡的getMethod要跟(1)中客戶端定義的@GET中的名稱一致,實現doGet表示接收Get請求,完後直接返回字串"haha"
(4)訪問請求並接收返回值
private void getTest() {
Call<String> call = RetroFactory.getStringService().getTest();
call.enqueue(new Callback<String>() {
@Override
public void onResponse(Call<String> call, Response<String> response) {
if (response.isSuccessful() && response.errorBody() == null) {
Log.d(TAG, "str:" + response.body().toString());
} else {
Log.d(TAG, "error code:" + response.code());
Log.d(TAG, "error message:" + response.message());
}
}
@Override
public void onFailure(Call<String> call, Throwable t) {
Log.d(TAG, "error:" + t.getMessage());
}
});
}
首先,定義一個Call物件,這在(1)和(2)中已經詳細說明,接下來呼叫enqueue方法,enqueue方法表示非同步請求,如果是同步則呼叫execute,這裡實現了CallBack的2個方法,onResponse和onFailure,在onResponse中,首先需要判斷下請求是否成功以及是否有錯誤資訊,這裡需要特別注意,單純從字面上理解,onFailure才是接收錯誤的地方,異常都應該在onFailure中處理,但其實並不完全是這樣,這裡的onFailure是接收網路的異常,比如說網路沒有連線,或者連線超時,這時會進入onFailure方法,但如果是網路正常,但是返回不正常,是會進入onResponse方法,比如,我將(2)中的基礎地址MyWeb改成MyWeb2,那這個url地址是不存在的,伺服器會報404的錯誤,但是不會進onFailure,而是進了onResponse,程式碼中的error
code會打印出404,而error message會打印出not found,這個解析錯誤的工作Retrofit已經幫我們做了,只是我們需要明確是進了onFailure還是onResponse方法。最後,當一切正常的時候,我們通過response的body,轉換成string,就可以得到(3)中伺服器返回的字串"haha"
大家可以看到,一個基礎的Retrofit請求是相當簡單的,去除程式碼中的日誌列印,寥寥幾行程式碼就可以實現了,下面看看Post請求,也很簡單:
(1)在RetrofitService介面中,增加要訪問的POST方法
@FormUrlEncoded
@POST("createUser")
Call<Void> createPerson(@Field("name") String name, @Field("age") String age);
首先,第一個註解FormUrlEncoded表示對POST請求的引數進行編碼,這是Retrofit2強制要求的,否則執行會報錯,@POST註解表示這是一個POST方法,createUser是請求的名稱,這裡假設我只提交引數,不需要返回值,所以Call裡的泛型是Void,完後方法裡定義了2個引數註解,名字和年齡,表示客戶端要傳的2個引數
(2)定義Servlet
@WebServlet(name="createUser",value="/createUser")
public class CreateUserServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String name = req.getParameter("name");
String age = req.getParameter("age");
System.out.println("name:" + name);
System.out.println("age:" + age);
}
}
這裡基本和上面的GET方法類似,只不過為了接收POST請求,這裡實現了doPost方法,完後接收客戶端傳遞過來的引數並列印(3)訪問請求
private void createPerson() {
Call<Void> call = RetroFactory.getStringService().createPerson("gesanri", "10");
call.enqueue(new Callback<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> response) {
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
}
});
}
呼叫(1)中定義的方法createPerson,完後傳了2個引數,最後呼叫enqueue方法啟動網路訪問,完後伺服器的Console中就可以看到列印
name:gesanri
age:10
可以看到,不管是GET還是POST請求,Retrofit都可以非常簡單的處理。
下面我們來考慮一個稍微複雜點的情況,更接近於真實應用中的場景,首先,訪問伺服器介面都是有返回的,而且不會返回一個簡單的字串,一般是一個json格式。我們假設伺服器返回的結果統一為如下格式:
{"code":0, "message":"123", "data:":泛型}
其中code是一個int,用0表示成功,非0表示失敗,message是成功或失敗的提示資訊,而data則是返回的實際結果,可以為任意型別
現在我們來定義一個POST請求,模擬客戶端請求伺服器的資料,返回一個Person物件列表,Person有2個引數,姓名和年齡,客戶端列印所有Person資訊並且在介面顯示第一個Person的資訊,返回成功的示例如下:
{"code":0, "message":"獲取使用者成功!", "data:":[{"name":"張三", "age":23},{"name":"李四", "age":28}]}
(1) 定義實體類Person
public class Person implements Serializable{
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
這就是上面提到的Person實體類物件,定義get和set方法,很簡單
(2) 定義實體類BaseEntity
public class BaseEntity<E> implements Serializable {
private int code;
private String message;
private E data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public E getData() {
return data;
}
public void setData(E data) {
this.data = data;
}
}
這個BaseEntity是我們通用的伺服器返回值物件,也就是上面說的json物件,注意這裡用到了泛型,因為code和message是固定的,而data物件是不固定的,伺服器可以返回任意型別的data物件
(3)定義POST請求
在上面提到的RetrofitService中,增加一個POST方法
@FormUrlEncoded
@POST("getUsers")
Call<BaseEntity<List<Person>>> getUsers(@FieldMap Map<String, String> map);
這裡的2個註解上面已經解釋過,主要看getUsers方法,首先,它的引數我們用了一個FieldMap,這裡傳了一個Map,在上面介紹基本POST請求中,我們是將引數一個接一個的加在引數列表裡,這在引數較少的時候可以,但如果引數比較多的話,接一排引數既不美觀也容易出錯,用FieldMap是不錯的選擇,另外Retrofit2也可以傳一個Body,不過這種需要定義一個實體類,用來包含所有的引數物件,所以綜合起來還是選用FieldMap。再來看返回值,Call物件裡面的泛型為<BaseEntity<List<Person>>>,也就是我們上面提到的,通用網路返回值型別,其中data引數的型別為List<Person>
(4)提供新的生成RetrofitService的方法
private static Retrofit jsonRetrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(JacksonConverterFactory.create())
.build();
public static RetrofitService getJsonService() {
RetrofitService service = jsonRetrofit.create(RetrofitService.class);
return service;
}
在介紹Get請求的時候,我們用到了ScalarsConverterFactory,它可以轉換成String型別,但現在我們的伺服器通用返回型別是json格式,所以我們需要一個新的能轉換json型別的轉換類,可以選用gson或jackson,在資料量較大的情況下,gson的效率相比jackson還是有較大差距,這裡選用jackson,所以重新生成一個Retrofit物件,用到JacksonConverterFactory的轉換類,並返回一個新的RetrofitService物件,注意這裡要記得在build.gradle中引入依賴compile
'com.squareup.retrofit2:converter-jackson:2.0.0'
(5)定義處理網路請求的公共類
public class BaseTask<T> {
private Call<BaseEntity<T>> mCall;
private Context mContext;
private final int SUCCESS = 0;
private final String TAG = "response";
public BaseTask(Context context, Call call) {
mCall = call;
mContext = context;
}
public void handleResponse(final ResponseListener listener) {
mCall.enqueue(new Callback<BaseEntity<T>>() {
@Override
public void onResponse(Call<BaseEntity<T>> call, Response<BaseEntity<T>> response) {
if (response.isSuccessful() && response.errorBody() == null) {
if (response.body().getCode() == SUCCESS) {
listener.onSuccess((T) response.body().getData());
} else {
Toast.makeText(mContext, response.body().getMessage(), Toast.LENGTH_LONG).show();
listener.onFail();
}
} else {
Log.d(TAG, "error code:" + response.code());
Log.d(TAG, "error message:" + response.message());
Toast.makeText(mContext, "網路請求返回異常!", Toast.LENGTH_LONG).show();
}
}
@Override
public void onFailure(Call<BaseEntity<T>> call, Throwable t) {
Log.d(TAG, "error:" + t.getMessage());
Toast.makeText(mContext, "網路請求出現異常!", Toast.LENGTH_LONG).show();
}
});
}
。
public interface ResponseListener<T> {
void onSuccess(T t);
void onFail();
}
}
Retrofit提供的網路請求的回撥格式是通用的,所以我們可以將其抽出來,寫在一個類中,避免所有的網路請求都去寫一些重複的程式碼,這裡的泛型T在這個介面中就是對應的List<Person>,我們真正需要處理的也就是這個物件,code和message都是輔助的功能,可以在公共類中處理。
這裡定義了一個內部介面ResponseListener,它包含兩個方法,onSuccess和onFail,對應在網路請求成功的前提下,資料的獲取成功和失敗,比如說,我這裡去請求獲取使用者資料,如果獲取成功,就進入onSuccess方法,如果使用者不存在,則進入onFail方法。
在建構函式中,我們接收Call物件,這裡將需要訪問的網路請求傳入,完後在handleResponse方法中,用call物件來請求網路,並接收和處理返回值,當code為0時,表示成功,回撥onSuccess方法,否則回撥onFail方法,至於其它的網路方面的異常情況,都可以在這裡處理
(6)定義Servlet
@WebServlet(name="getUsers",value="/getUsers")
public class GetUsersServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("id:" + req.getParameter("id"));
System.out.println("name:" + req.getParameter("name"));
resp.setCharacterEncoding("utf-8");
resp.getWriter().write("{\"code\":1, \"message\":\"獲取使用者不存在!\", \"data\":null}");
//resp.getWriter().write("{\"code\":0, \"message\":\"獲取使用者成功!\", \"data\":[{\"name\":\"張三\", \"age\":23},{\"name\":\"李四\", \"age\":28}]}");
}
}
這裡接收客戶端傳來的Map引數,打印出來,這裡就不查詢資料庫了,假設得到引數後,就返回結果,直接將json物件返回給客戶端
(7)客戶端訪問網路
private void getUsers() {
Map<String, String> map = new HashMap<String, String>();
map.put("id", "123");
map.put("name", "gesanri");
new BaseTask<List<Person>>(this, RetroFactory.getJsonService().getUsers(map)).handleResponse(new BaseTask.ResponseListener<List<Person>>() {
@Override
public void onSuccess(List<Person> o) {
for (int i = 0; i < o.size(); i++) {
Person person = o.get(i);
Log.d(TAG, "name:" + person.getName());
Log.d(TAG, "age:" + person.getAge());
}
}
@Override
public void onFail() {
}
});
}
可以看到,通過上面的封裝,客戶端的工作就輕鬆了很多,只需要新建一個BaseTask物件,並呼叫handleResponse方法來接收回調即可。
實際專案中,情況可能遠比上面複雜,這裡主要起到一個拋磚引玉的作用,萬事開頭難,有了基本的思路,後面的工作就好辦了。
原始碼下載