1 前言
??每次使用 Retrofit 做網(wǎng)絡請求都要到網(wǎng)上去搜索代碼,然后復制、粘貼;有時候某個注解的使用方式忘記了,也會查查這個注解的使用例子。查的多了就覺得煩了,所以想著自己總結(jié)一下,下次使用直接查自己的博客就好了。
??從本質(zhì)來看,Retrofit 網(wǎng)絡請求工作都是 OkHttp 做的,它只是對 OkHttp 的又一次封裝。
?? Retrofit 通過注解的方式,封裝了網(wǎng)絡請求的接口,所以學習 Retrofit 的關鍵是學會注解。那注解是用來干啥使的呢?注解其實是用來拼接網(wǎng)絡請求接口用的,也可以說是用來拼接報文的。所以在學習注解之前,應該先學習一下報文的知識。學完了報文再學注解,理解起注解的意思來就會非常容易了。
2 報文
2.1 報文的格式
??為什么要在學習 Retrofit 之前,要先學習報文呢?
??因為每一次網(wǎng)絡交互,其實都是報文的交互。App 發(fā)送請求報文給服務器,服務器收到請求后發(fā)送響應報文給App,App根據(jù)響應報文再解析出自己需要的數(shù)據(jù)進行顯示,這就完成了一次網(wǎng)絡交互。
??所以,只要知道了報文的格式,再熟悉一下 Retrofit 的注解,就能夠很容易理解、記憶 Retrofit 的使用方式了。
2.1.1 請求報文的格式
??先說一下 HTTP 請求報文的組成吧,一個HTTP請求報文由請求行、請求頭、空行和請求體四部分組成,其一般格式如圖一所示。

2.1.1.1 請求行
??請求行由請求方法、路徑和 HTTP 協(xié)議版本這三個字段組成,字段間使用空格間隔。圖一中的 POST 就是請求方法,/user/register 就是請求路徑,HTTP/1.1 就是 HTTP 協(xié)議版本。
??HTTP 協(xié)議從版本角度分為 HTTP 1.0 和 HTTP1.1。
??HTTP 1.0 請求方法:GET,POST 和 HEAD 。
??HTTP1.1 新增的請求方法:OPTIONS,PUT, DELETE,TRACE和 CONNECT。
??所以 ,HTTP 協(xié)議總共有 8 種請求方法。這里說的 HTTP 1.0 和 HTTP1.1 就是HTTP 協(xié)議的版本。
??下面記錄下各個請求方法的作用。
??【GET】 請求數(shù)據(jù)。請求指定的URL,并返回實體主體(常見的是返回 Json 串)。
??【POST】 新增或修改。向指定資源提交數(shù)據(jù)進行處理請求(例如提交表單或者上傳文件)。數(shù)據(jù)被包含在請求體中。POST請求可能會導致新的資源的建立和/或已有資源的修改。
??【HEAD】 獲取響應頭。類似于get請求,只不過返回的響應中沒有具體的內(nèi)容,用于獲取報頭
??【PUT】 修改。從客戶端向服務器傳送的數(shù)據(jù)取代指定的文檔的內(nèi)容。
??【DELETE】 刪除。請求服務器刪除指定的頁面。
??【CONNECT】 HTTP/1.1協(xié)議中預留給能夠?qū)⑦B接改為管道方式的代理服務器。
??【OPTIONS】 允許客戶端查看服務器的性能
??【TRACE】 回顯服務器收到的請求,主要用于測試或診斷
??雖然我們有這么多種請求方法,但常用的還是 GET、POST、HEAD 、PUT 和 DELETE 這五種方法。
??GET 請求用于從服務器獲取數(shù)據(jù),比如獲取 Json 格式的數(shù)據(jù),一般來說,GET 請求報文是沒有請求體的,
??POST 方法用于新增或修改資源,典型的應用就是用戶注冊,注冊一個新用戶,就是向服務器新增一個資源,POST 方法是有請求體的。
??HEAD 方法和 GET 方法類似,他們的區(qū)別就是 HEAD 用于獲取響應頭,它不返回響應體,而 GET 方法既會返回響應頭,也會返回響應體。
??PUT 方法用于修改服務器資源,典型應用就是修改昵稱,它和 POST 方法的區(qū)別是 POST 方法既可以修改資源,也可以新增資源,但 PUT 方法只能新增資源,不能修改資源。
??DELETE 方法用于刪除資源,沒什么可說的。
??說完了請求行中的請求方法,接著說說請求行中的路徑。那請求行中的路徑是怎么來的呢?我舉例說一下吧,如圖二所示,我有 https://www.wanandroid.com/user/register 這樣一個接口,那這個接口中的 https 就表示協(xié)議類型,www.wanandroid.com 就表示主機地址,/user/register 就是路徑了,這個解釋一目了然,我就不多說了。
??請求行中的協(xié)議版本前面已經(jīng)提過了,就不贅述了。

2.1.1.2 請求頭
??請求頭:請求頭由鍵值對組成,每一行就是一組鍵值對,鍵和值之間用英文冒號分隔,像圖一中的 Host: www.wanandroid.com 這樣就是一組請求頭。
??下面記錄一些比較常見的請求頭。
??User-Agent: 用戶代理,即是誰實際發(fā)送請求、接受響應的,例如?機瀏覽器、某款?機 App
??Host:主機名
??Content-Type: 指定 Body 的類型,比如 Content-Type: text/html; charset=utf-8、
Content-Type: application/x-www-form-urlencoded、Content-Type: multipart/form-data; boundary=----
Content-Type: application/json; charset=utf-8等等。
??Content-Encoding:壓縮類型。如 gzip
??Content-Length:請求體或響應體的長度 指定 Body 的長度(字節(jié))
??Accept:表示客戶端希望服務器返回什么類型的響應
??Accept-Charset: 客戶端接受的字符集。如 utf-8
??Accept-Encoding: 客戶端接受的壓縮編碼類型。如 gzip
??Accept-Charset:表示客戶端希望服務器返回的內(nèi)容的編碼格式
Location 指定重定向的?標 URL
2.1.2 響應報文的格式
??響應報文和請求報文的格式差不多,響應報文由狀態(tài)行、響應頭和響應體組成,響應報文格式如圖三所示。狀態(tài)行由 Http協(xié)議版本,響應碼和響應信息組成。
??響應頭和響應體的意思和請求報文一樣,所以也沒有什么可多說的了。

?? 下面著重說一下狀態(tài)碼。狀態(tài)碼由三位數(shù)字組成,第一個數(shù)字定義了響應的類別。
?? 1xx:指示信息--表示請求已接收,繼續(xù)處理。
?? 2xx:成功--表示請求已被成功接收、理解、接受。
?? 3xx:重定向--要完成請求必須進行更進一步的操作。
?? 4xx:客戶端錯誤--請求有語法錯誤或請求無法實現(xiàn)。
?? 5xx:服務器端錯誤--服務器未能實現(xiàn)合法的請求。
?? 常見狀態(tài)代碼、狀態(tài)描述的說明如下。
?? 200 OK:客戶端請求成功。
?? 400 Bad Request:客戶端請求有語法錯誤,不能被服務器所理解。
?? 401 Unauthorized:請求未經(jīng)授權(quán),這個狀態(tài)代碼必須和WWW-Authenticate報頭域一起使用。
?? 403 Forbidden:服務器收到請求,但是拒絕提供服務。
?? 404 Not Found:請求資源不存在,舉個例子:輸入了錯誤的URL。
?? 500 Internal Server Error:服務器發(fā)生不可預期的錯誤。
?? 503 Server Unavailable:服務器當前不能處理客戶端的請求,一段時間后可能恢復正常。
2.2 報文的實例
??現(xiàn)在我有這樣一個接口:https://www.wanandroid.com/user/register(該接口來自于鴻洋老師的玩 Android 開放 API),使用 Postman 發(fā)送一下 POST請求,如圖四所示,我們看到了該接口返回了 Json 數(shù)據(jù),說明我們請求成功了。

??接下來,通過點擊如圖四所示的紅框圈出來的“Code”,即可看到如圖五所示的網(wǎng)絡請求的報文,這時我們就看到了之前提到的那些東西,比如 Content-Type、Host 等。其實我們平時在用 OkHttp 做網(wǎng)絡請求的時候,也可以通過攔截器拿到這些東西。

??看完請求報文我們再看一下響應報文。不過,Postman將響應報文分開顯示了,沒有像請求報文一樣集中在一起。

??如圖六所示,狀態(tài)行、響應頭和響應體是分開的。
??圖六中紅框圈出來的 Body 代表響應體,我們 Android 端見得最多的響應體格式就是 Json 格式了吧,哈哈。
??圖六中紅框圈出來的 Headers 代表響應頭,比較常見的響應頭部有 Content-Type、Date等。
??圖六中紅框圈出來的 Status:200 OK 是狀態(tài)行的一部分,200是響應碼,OK 是響應信息,還缺一個沒顯示的是 Http 協(xié)議版本,它顯示不顯示對我們來說也不重要,反正用不到。
3 使用
??說了半天,終于開始寫 Retrofit 的使用了,真不容易啊?,F(xiàn)在如果我要請求這個接口的數(shù)據(jù):https://wanandroid.com/wxarticle/chapters/json ,我該怎么做呢?
??第一步,添加依賴
??新建一個 Project,在 app 的 build.gradle (不是 project 的 build.gradle)添加如下依賴:
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
??第二步,在 AndroidManifest.xml 里添加網(wǎng)絡權(quán)限。
<uses-permission android:name="android.permission.INTERNET" />
??第三步,新建實體類
??通過 Postman 請求接口,獲取返回的 Json 數(shù)據(jù),使用 GsonFormat(Android Studio 的插件) 生成實體類 ArticleModel 。
package isuperred.com.github.retrofit;
import java.util.List;
public class ArticleModel {
private int errorCode;
private String errorMsg;
private List<DataBean> data;
public int getErrorCode() {
return errorCode;
}
public void setErrorCode(int errorCode) {
this.errorCode = errorCode;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
public List<DataBean> getData() {
return data;
}
public void setData(List<DataBean> data) {
this.data = data;
}
public static class DataBean {
private int courseId;
private int id;
private String name;
private int order;
private int parentChapterId;
private boolean userControlSetTop;
private int visible;
private List<?> children;
public int getCourseId() {
return courseId;
}
public void setCourseId(int courseId) {
this.courseId = courseId;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
public int getParentChapterId() {
return parentChapterId;
}
public void setParentChapterId(int parentChapterId) {
this.parentChapterId = parentChapterId;
}
public boolean isUserControlSetTop() {
return userControlSetTop;
}
public void setUserControlSetTop(boolean userControlSetTop) {
this.userControlSetTop = userControlSetTop;
}
public int getVisible() {
return visible;
}
public void setVisible(int visible) {
this.visible = visible;
}
public List<?> getChildren() {
return children;
}
public void setChildren(List<?> children) {
this.children = children;
}
}
}
??第四步,添加接口
??新建存放請求數(shù)據(jù)的接口的類 ApiService。
package isuperred.com.github.retrofit;
import retrofit2.Call;
import retrofit2.http.GET;
public interface ApiService {
@GET("wxarticle/chapters/json")
Call<ArticleModel> getArticle();
}
??上面的 @GET 注解表示該接口使用 GET 請求方法,"wxarticle/chapters/json" 就是請求報文中請求行里 路徑 這個字段,Call 是固定不變的(如果使用 Rxjava,Call 會被替換為 Observable),泛型 ArticleModel 就是第三步新建的實體類,getArticle 是方法名。
??第五步,創(chuàng)建 Retrofit 管理類 RetrofitManager。下面的 BASE_URL 就是https://wanandroid.com/wxarticle/chapters/json 這個接口的協(xié)議和域名。注意一下,BASE_URL 最后是有“/”的,ApiService 接口的注解里的 wxarticle/chapters/json 最前面是沒有“/” 的。
package isuperred.com.github.retrofit;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class RetrofitManager {
private static RetrofitManager instance;
private static final String BASE_URL = "https://www.wanandroid.com/";
private Retrofit mRetrofit;
public RetrofitManager() {
mRetrofit = new Retrofit
.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
public static RetrofitManager getInstance() {
if (instance == null) {
synchronized (RetrofitManager.class) {
instance = new RetrofitManager();
}
}
return instance;
}
public <T> T create(final Class<T> service) {
return mRetrofit.create(service);
}
}
??第六步,在 Activity 里調(diào)用 Retrofit 方法進行異步或同步請求網(wǎng)絡數(shù)據(jù)。注意同步請求會阻塞 UI 線程,所以同步請求應在子線程中執(zhí)行。
package isuperred.com.github.retrofit;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import java.io.IOException;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btnEnqueue = findViewById(R.id.btn_enqueue);
Button btnExecute = findViewById(R.id.btn_execute);
btnEnqueue.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//創(chuàng)建ApiService對象
ApiService apiService = RetrofitManager.getInstance().create(ApiService.class);
Call<ArticleModel> article = apiService.getArticle();
//異步請求數(shù)據(jù)
article.enqueue(new Callback<ArticleModel>() {
@Override
public void onResponse(Call<ArticleModel> call, Response<ArticleModel> response) {
ArticleModel articleModel = response.body();
if (articleModel == null) {
return;
}
List<ArticleModel.DataBean> dataBeans = articleModel.getData();
for (int i = 0; i < dataBeans.size(); i++) {
Log.e(TAG, "onResponse Name: " + dataBeans.get(i).getName());
}
}
@Override
public void onFailure(Call<ArticleModel> call, Throwable t) {
t.printStackTrace();
}
});
}
});
btnExecute.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
//創(chuàng)建ApiService對象
ApiService apiService = RetrofitManager.getInstance().create(ApiService.class);
Call<ArticleModel> article = apiService.getArticle();
ArticleModel articleModel = null;
try {
//同步請求數(shù)據(jù)
articleModel = article.execute().body();
} catch (IOException e) {
e.printStackTrace();
}
if (articleModel == null) {
return;
}
List<ArticleModel.DataBean> dataBeans = articleModel.getData();
for (int i = 0; i < dataBeans.size(); i++) {
Log.e(TAG, "onResponse Name: " + dataBeans.get(i).getName());
}
}
}).start();
}
});
}
}
??其對應的布局文件 activity_main.xml 如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_enqueue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="異步請求"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_execute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="同步請求"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_enqueue" />
</android.support.constraint.ConstraintLayout>
4 總結(jié)
??先寫這么多吧,注解下次再寫了。寫東西真麻煩啊,越寫越多,越寫越多,我都沒想到報文的內(nèi)容寫了這么久。關鍵我都寫煩了,還覺得自己沒寫清楚呢。真的是覺得把一個東西寫清楚是需要功力的,我還沒有功力啊。。。我就不瞎感慨了,早點回去洗衣服了!