1. 程式人生 > >Retrofit2的簡單應用與封裝

Retrofit2的簡單應用與封裝

Retrofit出來有一段時間了,我最近才知道有這個框架,之前一直使用的Volley,知道Retrofit後就試用了一下,感覺還是挺不錯的,使用起來比Volley更方便,封裝也比較簡單,下面先簡單介紹下它的基本使用方法

我用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)中定義的介面

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;
    }
}
baseUrl是網路請求的基礎地址,192.168.0.105是我本機的IP,8082是伺服器埠號,我是使用的湯姆貓,其它也可以,看個人習慣,MyWeb是新建的一個Web專案,用來接收客戶端請求。接下來,生成一個Retrofit物件,這裡主要指定2個引數,一個就是網路請求的基礎地址,另一個就是轉換工廠,注意,1.*版本預設使用Gson,而2.0版本則必須明確指定解析服務端相應的轉換方式,我現在服務端返回給客戶端的就是一個簡單的字串,所以我選用Scalars,具體轉換方式有幾種:
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方法來接收回調即可。

實際專案中,情況可能遠比上面複雜,這裡主要起到一個拋磚引玉的作用,萬事開頭難,有了基本的思路,後面的工作就好辦了。

原始碼下載