[譯] 網路請求框架 Retrofit 2 使用入門

  • 原文地址:Get Started With Retrofit 2 HTTP Client
  • 原文作者:Chike Mgbemena
  • 譯文出自:掘金翻譯計劃
  • 譯者:Zhiw
  • 校對者:PhxNirvana,Draftbk

你將要創造什麼

Retrofit 是什麼?

Retrofit 是一個用於 Android 和 Java 平台的類型安全的網路請求框架。Retrofit 通過將 API 抽象成 Java 介面而讓我們連接到 REST web 服務變得很輕鬆。在這個教程里,我會向你介紹如何使用這個 Android 上最受歡迎和經常推薦的網路請求庫之一。

這個強大的庫可以很簡單的把返回的 JSON 或者 XML 數據解析成簡單 Java 對象(POJO)。GET, POST, PUT, PATCH, 和 DELETE 這些請求都可以執行。

和大多數開源軟體一樣,Retrofit 也是建立在一些強大的庫和工具基礎上的。Retrofit 背後用了同一個開發團隊的 OkHttp 來處理網路請求。而且 Retrofit 不再內置 JSON 轉換器來將 JSON 裝換為 Java 對象。取而代之的是提供以下 JSON 轉換器來處理:

  • Gson: com.squareup.retrofit:converter-gson
  • Jackson: com.squareup.retrofit:converter-jackson
  • Moshi: com.squareup.retrofit:converter-moshi

對於 Protocol Buffers, Retrofit 提供了:

  • Protobuf: com.squareup.retrofit2:converter-protobuf

  • Wire: com.squareup.retrofit2:converter-wire

對於 XML 解析, Retrofit 提供了:

  • Simple Framework: com.squareup.retrofit2:converter-simpleframework

那麼我們為什麼要用 Retrofit 呢?

開發一個自己的用於請求 REST API 的類型安全的網路請求庫是一件很痛苦的事情:你需要處理很多功能,比如建立連接,處理緩存,重連接失敗請求,線程,響應數據的解析,錯誤處理等等。從另一方面來說,Retrofit 是一個有優秀的計劃,文檔和測試並且經過考驗的庫,它會幫你節省你的寶貴時間以及不讓你那麼頭痛。

在這個教程里,我會構建一個簡單的應用,根據 Stack Exchange API 查詢上面最近的回答,從而來教你如何使用 Retrofit 2 來處理網路請求。我們會指明 /answers 這樣一個路徑,然後拼接到 base URL api.stackexchange.com/2/ 上執行一個 GET 請求——然後我們會得到響應結果並且顯示到 RecyclerView 上。我還會向你展示如何利用 RxJava 來輕鬆地管理狀態和數據流。

1.創建一個 Android Studio 工程

打開 Android Studio,創建一個新工程,然後創建一個命名為 MainActivity 的空白 Activity。

2. 添加依賴

創建一個新的工程後,在你的 build.gradle 文件裡面添加以下依賴。這些依賴包括 RecyclerView,Retrofit 庫,還有 Google 出品的將 JSON 裝換為 POJO(簡單 Java 對象)的 Gson 庫,以及 Retrofit 的 Gson。

// Retrofitncompile com.squareup.retrofit2:retrofit:2.1.0nn// JSON Parsingncompile com.google.code.gson:gson:2.6.1ncompile com.squareup.retrofit2:converter-gson:2.1.0nn// recyclerviewncompile com.android.support:recyclerview-v7:25.0.1n

不要忘記同步(sync)工程來下載這些庫。

3. 添加網路許可權

要執行網路操作,我們需要在應用的清單文件 AndroidManifest.xml 裡面聲明網路許可權。

<?xml version="1.0" encoding="utf-8"?>n<manifest xmlns:android="http://schemas.android.com/apk/res/android"n package="com.chikeandroid.retrofittutorial">nn <uses-permission android:name="android.permission.INTERNET" />nn <applicationn android:allowBackup="true"n android:icon="@mipmap/ic_launcher"n android:label="@string/app_name"n android:supportsRtl="true"n android:theme="@style/AppTheme">n <activity android:name=".MainActivity">n <intent-filter>n <action android:name="android.intent.action.MAIN"/>nn <category android:name="android.intent.category.LAUNCHER"/>n </intent-filter>n </activity>n </application>nn</manifest>n

4.自動生成 Java 對象

我們利用一個非常有用的工具來幫我們將返回的 JSON 數據自動生成 Java 對象:jsonschema2pojo。

取得示例的 JSON 數據

複製粘貼 api.stackexchange.com/2 到你的瀏覽器地址欄,或者如果你熟悉的話,你可以使用 Postman 這個工具。然後點擊 Enter —— 它將會根據那個地址執行一個 GET 請求,你會看到返回的是一個 JSON 對象數組,下面的截圖是使用了 Postman 的 JSON 響應結果。

{n "items": [n {n "owner": {n "reputation": 1,n "user_id": 6540831,n "user_type": "registered",n "profile_image": "https://www.gravatar.com/avatar/6a468ce8a8ff42c17923a6009ab77723?s=128&d=identicon&r=PG&f=1",n "display_name": "bobolafrite",n "link": "http://stackoverflow.com/users/6540831/bobolafrite"n },n "is_accepted": false,n "score": 0,n "last_activity_date": 1480862271,n "creation_date": 1480862271,n "answer_id": 40959732,n "question_id": 35931342n },n {n "owner": {n "reputation": 629,n "user_id": 3054722,n "user_type": "registered",n "profile_image": "https://www.gravatar.com/avatar/0cf65651ae9a3ba2858ef0d0a7dbf900?s=128&d=identicon&r=PG&f=1",n "display_name": "jeremy-denis",n "link": "http://stackoverflow.com/users/3054722/jeremy-denis"n },n "is_accepted": false,n "score": 0,n "last_activity_date": 1480862260,n "creation_date": 1480862260,n "answer_id": 40959731,n "question_id": 40959661n },n ...n ],n "has_more": true,n "backoff": 10,n "quota_max": 300,n "quota_remaining": 241n }n

從你的瀏覽器或者 Postman 複製 JSON 響應結果。

將 JSON 數據映射到 Java 對象

現在訪問 jsonschema2pojo,然後粘貼 JSON 響應結果到輸入框。

選擇 Source Type 為 JSON,Annotation Style 為 Gson,然後取消勾選 Allow additional properties

然後點擊 Preview 按鈕來生成 Java 對象。

你可能想知道在生成的代碼裡面, @SerializedName 和 @Expose 是幹什麼的。別著急,我會一一解釋的。

Gson 使用 @SerializedName 註解來將 JSON 的 key 映射到我們類的變數。為了與 Java 對類成員屬性的駝峰命名方法保持一致,不建議在變數中使用下劃線將單詞分開。@SerializeName 就是兩者的翻譯官。

@SerializedName("quota_remaining")n@Exposenprivate Integer quotaRemaining;n

在上面的示例中,我們告訴 Gson 我們的 JSON 的 key quota_remaining 應該被映射到 Java 變數 quotaRemaining上。如果兩個值是一樣的,即如果我們的 JSON 的 key 和 Java 變數一樣是 quotaRemaining,那麼就沒有必要為變數設置 @SerializedName 註解,Gson 會自己搞定。

@Expose 註解表明在 JSON 序列化或反序列化的時候,該成員應該暴露給 Gson。

將數據模型導入 Android Studio

現在讓我們回到 Android Studio。新建一個 data 的子包,在 data 裡面再新建一個 model 的包。在 model 包裡面,新建一個 Owner 的 Java 類。

然後將 jsonschema2pojo 生成的 Owner 類複製粘貼到剛才新建的 Owner 類文件裡面。

import com.google.gson.annotations.Expose;nimport com.google.gson.annotations.SerializedName;nnpublic class Owner {nn @SerializedName("reputation")n @Exposen private Integer reputation;n @SerializedName("user_id")n @Exposen private Integer userId;n @SerializedName("user_type")n @Exposen private String userType;n @SerializedName("profile_image")n @Exposen private String profileImage;n @SerializedName("display_name")n @Exposen private String displayName;n @SerializedName("link")n @Exposen private String link;n @SerializedName("accept_rate")n @Exposen private Integer acceptRate;nn public Integer getReputation() {n return reputation;n }nn public void setReputation(Integer reputation) {n this.reputation = reputation;n }nn public Integer getUserId() {n return userId;n }nn public void setUserId(Integer userId) {n this.userId = userId;n }nn public String getUserType() {n return userType;n }nn public void setUserType(String userType) {n this.userType = userType;n }nn public String getProfileImage() {n return profileImage;n }nn public void setProfileImage(String profileImage) {n this.profileImage = profileImage;n }nn public String getDisplayName() {n return displayName;n }nn public void setDisplayName(String displayName) {n this.displayName = displayName;n }nn public String getLink() {n return link;n }nn public void setLink(String link) {n this.link = link;n }nn public Integer getAcceptRate() {n return acceptRate;n }nn public void setAcceptRate(Integer acceptRate) {n this.acceptRate = acceptRate;n }n}n

利用同樣的方法從 jsonschema2pojo 複製過來,新建一個 Item 類。

import com.google.gson.annotations.Expose;nimport com.google.gson.annotations.SerializedName;nnpublic class Item {nn @SerializedName("owner")n @Exposen private Owner owner;n @SerializedName("is_accepted")n @Exposen private Boolean isAccepted;n @SerializedName("score")n @Exposen private Integer score;n @SerializedName("last_activity_date")n @Exposen private Integer lastActivityDate;n @SerializedName("creation_date")n @Exposen private Integer creationDate;n @SerializedName("answer_id")n @Exposen private Integer answerId;n @SerializedName("question_id")n @Exposen private Integer questionId;n @SerializedName("last_edit_date")n @Exposen private Integer lastEditDate;nn public Owner getOwner() {n return owner;n }nn public void setOwner(Owner owner) {n this.owner = owner;n }nn public Boolean getIsAccepted() {n return isAccepted;n }nn public void setIsAccepted(Boolean isAccepted) {n this.isAccepted = isAccepted;n }nn public Integer getScore() {n return score;n }nn public void setScore(Integer score) {n this.score = score;n }nn public Integer getLastActivityDate() {n return lastActivityDate;n }nn public void setLastActivityDate(Integer lastActivityDate) {n this.lastActivityDate = lastActivityDate;n }nn public Integer getCreationDate() {n return creationDate;n }nn public void setCreationDate(Integer creationDate) {n this.creationDate = creationDate;n }nn public Integer getAnswerId() {n return answerId;n }nn public void setAnswerId(Integer answerId) {n this.answerId = answerId;n }nn public Integer getQuestionId() {n return questionId;n }nn public void setQuestionId(Integer questionId) {n this.questionId = questionId;n }nn public Integer getLastEditDate() {n return lastEditDate;n }nn public void setLastEditDate(Integer lastEditDate) {n this.lastEditDate = lastEditDate;n }n}n

最後,為返回的 StackOverflow 回答新建一個 SOAnswersResponse 類。注意在 jsonschema2pojo 裡面類名是 Example,別忘記把類名改成 SOAnswersResponse。

import com.google.gson.annotations.Expose;nimport com.google.gson.annotations.SerializedName;nnimport java.util.List;nnpublic class SOAnswersResponse {nn @SerializedName("items")n @Exposen private List<Item> items = null;n @SerializedName("has_more")n @Exposen private Boolean hasMore;n @SerializedName("backoff")n @Exposen private Integer backoff;n @SerializedName("quota_max")n @Exposen private Integer quotaMax;n @SerializedName("quota_remaining")n @Exposen private Integer quotaRemaining;nn public List<Item> getItems() {n return items;n }nn public void setItems(List<Item> items) {n this.items = items;n }nn public Boolean getHasMore() {n return hasMore;n }nn public void setHasMore(Boolean hasMore) {n this.hasMore = hasMore;n }nn public Integer getBackoff() {n return backoff;n }nn public void setBackoff(Integer backoff) {n this.backoff = backoff;n }nn public Integer getQuotaMax() {n return quotaMax;n }nn public void setQuotaMax(Integer quotaMax) {n this.quotaMax = quotaMax;n }nn public Integer getQuotaRemaining() {n return quotaRemaining;n }nn public void setQuotaRemaining(Integer quotaRemaining) {n this.quotaRemaining = quotaRemaining;n }n}n

5. 創建 Retrofit 實例

為了使用 Retrofit 向 REST API 發送一個網路請求,我們需要用 Retrofit.Builder 類來創建一個實例,並且配置一個 base URL。

在 data 包裡面新建一個 remote 的包,然後在 remote 包裡面新建一個 RetrofitClient 類。這個類會創建一個 Retrofit 的單例。Retrofit 需要一個 base URL 來創建實例。所以我們在調用 RetrofitClient.getClient(String baseUrl) 時會傳入一個 URL 參數。參見 13 行,這個 URL 用於構建 Retrofit 的實例。參見 14 行,我們也需要指明一個我們需要的 JSON converter(Gson)。

import retrofit2.Retrofit;nimport retrofit2.converter.gson.GsonConverterFactory;nnpublic class RetrofitClient {nn private static Retrofit retrofit = null;nn public static Retrofit getClient(String baseUrl) {n if (retrofit==null) {n retrofit = new Retrofit.Builder()n .baseUrl(baseUrl)n .addConverterFactory(GsonConverterFactory.create())n .build();n }n return retrofit;n }n}n

6.創建 API 介面

在 remote 包裡面,創建一個 SOService 介面,這個介面包含了我們將會用到用於執行網路請求的方法,比如 GET, POST, PUT, PATCH, 以及 DELETE。在該教程裡面,我們將執行一個 GET 請求。

import com.chikeandroid.retrofittutorial.data.model.SOAnswersResponse;nnimport java.util.List;nnimport retrofit2.Call;nimport retrofit2.http.GET;nnpublic interface SOService {nn @GET("/answers?order=desc&sort=activity&site=stackoverflow")n Call<List<SOAnswersResponse>> getAnswers();nn @GET("/answers?order=desc&sort=activity&site=stackoverflow")n Call<List<SOAnswersResponse>> getAnswers(@Query("tagged") String tags);n}n

GET 註解明確的定義了當該方法調用的時候會執行一個 GET 請求。介面里每一個方法都必須有一個 HTTP 註解,用於提供請求方法和相對的 URL。Retrofit 內置了 5 種註解:@GET, @POST, @PUT, @DELETE, 和 @HEAD。

在第二個方法定義中,我們添加一個 query 參數用於從服務端過濾數據。Retrofit 提供了 @Query("key") 註解,這樣就不用在地址裡面直接寫了。key 的值代表了 URL 里參數的名字。Retrofit 會把他們添加到 URL 裡面。比如說,如果我們把 android 作為參數傳遞給 getAnswers(String tags) 方法,完整的 URL 將會是:

https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow&tagged=androidn

介面方法的參數有以下注解:

nnnn@Pathn替換 API 地址中的變數n@Queryn通過註解的名字指明 query 參數的名字n@BodynPOST 請求的請求體n@Headern通過註解的參數值指明 headern

7.創建 API 工具類

現在我們要新建一個工具類。我們命名為 ApiUtils。該類設置了一個 base URL 常量,並且通過靜態方法 getSOService() 為應用提供 SOService 介面。

public class ApiUtils {nn public static final String BASE_URL = "https://api.stackexchange.com/2.2/";nn public static SOService getSOService() {n return RetrofitClient.getClient(BASE_URL).create(SOService.class);n }n}n

8.顯示到 RecyclerView

既然結果要顯示到 RecyclerView 上面,我們需要一個 adpter。以下是 AnswersAdapter 類的代碼片段。

public class AnswersAdapter extends RecyclerView.Adapter<AnswersAdapter.ViewHolder> {nn private List<Item> mItems;n private Context mContext;n private PostItemListener mItemListener;nn public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{nn public TextView titleTv;n PostItemListener mItemListener;nn public ViewHolder(View itemView, PostItemListener postItemListener) {n super(itemView);n titleTv = (TextView) itemView.findViewById(android.R.id.text1);nn this.mItemListener = postItemListener;n itemView.setOnClickListener(this);n }nn @Overriden public void onClick(View view) {n Item item = getItem(getAdapterPosition());n this.mItemListener.onPostClick(item.getAnswerId());nn notifyDataSetChanged();n }n }nn public AnswersAdapter(Context context, List<Item> posts, PostItemListener itemListener) {n mItems = posts;n mContext = context;n mItemListener = itemListener;n }nn @Overriden public AnswersAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {nn Context context = parent.getContext();n LayoutInflater inflater = LayoutInflater.from(context);nn View postView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false);nn ViewHolder viewHolder = new ViewHolder(postView, this.mItemListener);n return viewHolder;n }nn @Overriden public void onBindViewHolder(AnswersAdapter.ViewHolder holder, int position) {nn Item item = mItems.get(position);n TextView textView = holder.titleTv;n textView.setText(item.getOwner().getDisplayName());n }nn @Overriden public int getItemCount() {n return mItems.size();n }nn public void updateAnswers(List<Item> items) {n mItems = items;n notifyDataSetChanged();n }nn private Item getItem(int adapterPosition) {n return mItems.get(adapterPosition);n }nn public interface PostItemListener {n void onPostClick(long id);n }n}n

9.執行請求

在 MainActivity 的 onCreate() 方法內部,我們初始化 SOService 的實例(參見第 9 行),RecyclerView 以及 adapter。最後我們調用 loadAnswers() 方法。

private AnswersAdapter mAdapter;n private RecyclerView mRecyclerView;n private SOService mService;nn @Overriden protected void onCreate (Bundle savedInstanceState) {n super.onCreate( savedInstanceState );n setContentView(R.layout.activity_main );n mService = ApiUtils.getSOService();n mRecyclerView = (RecyclerView) findViewById(R.id.rv_answers);n mAdapter = new AnswersAdapter(this, new ArrayList<Item>(0), new AnswersAdapter.PostItemListener() {nn @Overriden public void onPostClick(long id) {n Toast.makeText(MainActivity.this, "Post id is" + id, Toast.LENGTH_SHORT).show();n }n });nn RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);n mRecyclerView.setLayoutManager(layoutManager);n mRecyclerView.setAdapter(mAdapter);n mRecyclerView.setHasFixedSize(true);n RecyclerView.ItemDecoration itemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST);n mRecyclerView.addItemDecoration(itemDecoration);nn loadAnswers();n }n

loadAnswers() 方法通過調用 enqueue() 方法來進行網路請求。當響應結果返回的時候,Retrofit 會幫我們把 JSON 數據解析成一個包含 Java 對象的 list(這是通過 GsonConverter 實現的)。

public void loadAnswers() {n mService.getAnswers().enqueue(new Callback<SOAnswersResponse>() {n @Overriden public void onResponse(Call<SOAnswersResponse> call, Response<SOAnswersResponse> response) {nn if(response.isSuccessful()) {n mAdapter.updateAnswers(response.body().getItems());n Log.d("MainActivity", "posts loaded from API");n }else {n int statusCode = response.code();n // handle request errors depending on status coden }n }nn @Overriden public void onFailure(Call<SOAnswersResponse> call, Throwable t) {n showErrorMessage();n Log.d("MainActivity", "error loading from API");nn }n});n}n

10. 理解 enqueue()

enqueue() 會發送一個非同步請求,當響應結果返回的時候通過回調通知應用。因為是非同步請求,所以 Retrofit 將在後台線程處理,這樣就不會讓 UI 主線程堵塞或者受到影響。

要使用 enqueue(),你必須實現這兩個回調方法:

  • onResponse()
  • onFailure()

只有在請求有響應結果的時候才會調用其中一個方法。

  • onResponse():接收到 HTTP 響應時調用。該方法會在響應結果能夠被正確地處理的時候調用,即使伺服器返回了一個錯誤信息。所以如果你收到了一個 404 或者 500 的狀態碼,這個方法還是會調用。為了拿到狀態碼以便後續的處理,你可以使用 response.code() 方法。你也可以使用 isSuccessful() 來確定返回的狀態碼是否在 200-300 範圍內,該範圍的狀態碼也表示響應成功。
  • onFailure():在與伺服器通信的時候發生網路異常或者在處理請求或響應的時候發生異常的時候調用。

要執行同步請求,你可以使用 execute() 方法。要注意同步請求在主線程會阻塞用戶的任何操作。所以不要在主線程執行同步請求,要在後台線程執行。

11.測試應用

現在你可以運行應用了。

12. 結合 RxJava

如果你是 RxJava 的粉絲,你可以通過 RxJava 很簡單的實現 Retrofit。RxJava 在 Retrofit 1 中是默認整合的,但是在 Retrofit 2 中需要額外添加依賴。Retrofit 附帶了一個默認的 adapter 用於執行 Call 實例,所以你可以通過 RxJava 的 CallAdapter 來改變 Retrofit 的執行流程。

第一步

添加依賴。

compile io.reactivex:rxjava:1.1.6ncompile io.reactivex:rxandroid:1.2.1ncompile com.squareup.retrofit2:adapter-rxjava:2.1.0n

第二步

在創建新的 Retrofit 實例的時候添加一個新的 CallAdapter RxJavaCallAdapterFactory.create()。

public static Retrofit getClient(String baseUrl) {n if (retrofit==null) {n retrofit = new Retrofit.Builder()n .baseUrl(baseUrl)n .addCallAdapterFactory(RxJavaCallAdapterFactory.create())n .addConverterFactory(GsonConverterFactory.create())n .build();n }n return retrofit;n}n

第三步

當我們執行請求時,我們的匿名 subscriber 會響應 observable 發射的事件流,在本例中,就是 SOAnswersResponse。當 subscriber 收到任何發射事件的時候,就會調用 onNext() 方法,然後傳遞到我們的 adapter。

@Overridenpublic void loadAnswers() {n mService.getAnswers().subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())n .subscribe(new Subscriber<SOAnswersResponse>() {n @Overriden public void onCompleted() {nn }nn @Overriden public void onError(Throwable e) {nn }nn @Overriden public void onNext(SOAnswersResponse soAnswersResponse) {n mAdapter.updateAnswers(soAnswersResponse.getItems());n }n });n}n

查看 Ashraff Hathibelagal 的 Getting Started With ReactiveX on Android 以了解更多關於 RxJava 和 RxAndroid 的內容。

總結

在該教程里,你已經了解了使用 Retrofit 的理由以及方法。我也解釋了如何將 RxJava 結合 Retrofit 使用。在我的下一篇文章中,我將為你展示如何執行 POST, PUT, 和 DELETE 請求,如何發送 Form-Urlencoded 數據,以及如何取消請求。

要了解更多關於 Retrofit 的內容,請參考 官方文檔。同時,請查看我們其他一些關於 Android 應用開發的課程和教程。


推薦閱讀:

Android手機 全面屏(18:9屏幕)適配指南
跪求android藍牙小車的上位機程序?最好能具體的,謝了
聖誕節特別版 - 遊戲也要過聖誕 #iOS #Android

TAG:Android | 稀土掘金 | 框架 |