同步請求和異步請求
Retrofit支持同步請求和異步請求。
同步請求
使用的同步請求通過定義返回類型來聲明。 下面的示例期望執行getTasks方法時返回Task列表。
public interface TaskService {
@GET("/tasks")
Call<List<Task>> getTasks();
}
在Retrofit 2中,每個請求都被包裝到一個Call對象中。 實際的同步或異步請求將在稍后創建的調用對象上的所需方法執行。 但是,同步和異步請求的接口類在Retrofit 2中是相同的。
同步方法在主線程上執行。 這意味阻塞UI,并且在此期間不可能進行交互。
Note: 警告:同步請求會觸發Android 4.0或更高版本上的應用崩潰。 你會遇到
NetworkOnMainThreadException
錯誤。
同步方法能夠直接返回值,因為操作在網絡請求期間阻止其他任何操作。
對于非阻塞UI,你必須在一個單獨的線程中的請求執行。 這意味著,你仍然可以在等待響應時與應用程序本身進行交互。
Get Results from Synchronous Requests
以下代碼片段說明了使用Retrofit執行的同步請求:
TaskService taskService = ServiceGenerator.createService(TaskService.class);
Call<List<Task>> call = taskService.getTasks();
List<Task>> tasks = call.execute().body();
在Call對象上使用.execute()方法,在Retrofit2中執行同步請求。反序列化響應主體可通過響應對象上的.body()方法獲得。
Asynchronous Requests
public interface TaskService {
@GET("/tasks")
Call<List<Task>> getTasks();
}
Retrofit在單獨的線程中執行。Callback類是通用的,并映射您定義的返回類型。 我們的示例返回一個任務列表,Callback在內部進行映射。
如上所述:Retrofit 2中的接口定義對于同步和異步請求是相同的。 所需的返回類型被封裝到一個Call對象中,實際的請求執行定義它的類型(同步/異步)。
Get Results from Asynchronous Requests
使用異步請求必須實現兩個回調方法:成功和失敗。 當從服務類調用異步getTasks()方法時,必須實現回調,并定義一旦請求完成應該做什么:
TaskService taskService = ServiceGenerator.createService(TaskService.class);
Call<List<Task>> call = taskService.getTasks();
call.enqueue(new Callback<List<Task>>() {
@Override
public void onResponse(Call<List<Task>> call, Response<List<Task>> response) {
if (response.isSuccessful()) {
// tasks available
} else {
// error response, no access to resource?
}
}
@Override
public void onFailure(Call<List<Task>> call, Throwable t) {
// something went completely south (like no internet connection)
Log.d("Error", t.getMessage());
}
}
Get Raw HTTP Response
如果你需要原始的HTTP響應對象,只需要定義返回類型為Response.
你可以接收Retrofit 2中原始響應主體與定義請求類型(sync / async)的方式相同。 不需要將Response類定義為返回類型,但可以在onResponse()回調方法中捕獲它。 讓我們看看下面的代碼片段來說明如何獲得原始響應:
call.enqueue(new Callback<List<Task>>() {
@Override
public void onResponse(Call<List<Task>> call, Response<List<Task>> response) {
// get raw response
Response raw = response.raw();
}
@Override
public void onFailure(Call<List<Task>> call, Throwable t) {}
}
在請求體中發送對象
Send Objects as Request Body
Retrofit提供了在請求體內發送對象的能力。 通過使用@Body注釋,可以指定對象用作HTTP請求主體。
public interface TaskService {
@POST("/tasks")
Call<Task> createTask(@Body Task task);
}
定義的RestAdapter的轉換器(如Gson)會將對象映射到JSON,它將最終作為請求的主體發送到服務器。
Example
public class Task {
private long id;
private String text;
public Task(long id, String text) {
this.id = id;
this.text = text;
}
}
創建一個新的Task對象。
Task task = new Task(1, "my task title");
Call<Task> call = taskService.createTask(task);
call.enqueue(new Callback<Task>() {});
調用方法createTask會將Task轉換為JSON。Task的JSON將如下所示:
{
"id": 1,
"text": "my task title"
}
添加自定義頭信息
Retrofit提供了兩個定義HTTP請求標頭字段的選項:靜態和動態。 靜態請求頭不能更改。請求頭的的鍵和值是固定的,并與應用程序同時啟動。
相反,動態請求頭必須每次都設置。
靜態請求頭
將API方法的頭和相應的值定義為注解。 對于使用此方法的每個請求,頭信息會自動通過Retrofit添加。 注解必須是鍵值對,可以有一個或者多個:
public interface UserService {
@Headers("Cache-Control: max-age=640000")
@GET("/tasks")
Call<List<Task>> getTasks();
}
上面的示例顯示了靜態頭的鍵值定義。 此外,您可以將多個鍵值字符串作為封裝在大括號{}中的列表傳遞到@Headers注釋。
public interface UserService {
@Headers({
"Accept: application/vnd.yourapi.v1.full+json",
"User-Agent: Your-App-Name"
})
@GET("/tasks/{task_id}")
Call<Task> getTask(@Path("task_id") long taskId);
}
此外,您可以通過Retrofit的RequestInterceptor的攔截方法(在Retrofit 2中自定義實現Interceptor接口)來定義靜態頭。
在Retrofit中必須在OkHttp中添加攔截器。
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
httpClient.addInterceptor(new Interceptor() {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request original = chain.request();
Request request = original.newBuilder()
.header("User-Agent", "Your-App-Name")
.header("Accept", "application/vnd.yourapi.v1.full+json")
.method(original.method(), original.body())
.build();
return chain.proceed(request);
}
}
OkHttpClient client = httpClient.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
上面的示例將User-Agent和Accept頭字段設置為相應的值。 這些值與使用RestAdapter(Retrofit 2中的Retrofit)和集成的RequestInterceptor(Retrofit 2中的Interceptor)執行的請求一起傳遞。
Dynamic Header
動態header是作為參數傳進方法中的。 在執行請求之前,通過Retrofit映射提供的參數值:
public interface UserService {
@GET("/tasks")
Call<List<Task>> getTasks(@Header("Content-Range") String contentRange);
}
動態請求頭允許你為每個請求設置不同的值。
復寫Retrofit2中的已經存在的請求頭
- .header(key,value):如果已經存在由鍵標識的現有頭,則用值覆蓋相應的值
- .addHeader(key,value):添加相應的標題鍵和值,即使存在具有相同鍵的現有標題字段
在攔截器中管理請求頭
添加請求頭
一個常見的例子是使用授權頭字段。如果幾乎每個請求都需要包含授權的頭字段,則可以使用攔截器來添加這條信息。 這樣,就不需要為每個端點聲明添加@Header注釋。
類似于前邊介紹的,可以直接添加頭信息,也可以覆蓋原有的頭信息。
How to Override Headers
使用OkHttp攔截器允許你修改實際請求。請求構建器具有一個.header(key,val)方法,它會將定義的頭添加到請求中。 如果已經存在具有相同鍵標識符的現有頭,則此方法將覆蓋先前定義的值。
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
httpClient.addInterceptor(new Interceptor() {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request original = chain.request();
// Request customization: add request headers
Request.Builder requestBuilder = original.newBuilder()
.header("Authorization", "auth-value"); // <-- this is the important line
Request request = requestBuilder.build();
return chain.proceed(request);
}
});
OkHttpClient client = httpClient.build();
Retrofit和OkHttp允許你添加多個具有相同key的頭信息,這將會覆蓋原有的頭信息。
How to Not Override Headers
有時會使用具有相同名稱的多個頭。實際上,我們只知道一個具體的實例:Cache-Control頭。 HTTP RFC2616指定允許具有相同名稱的多個標頭值,如果它們可以表示為逗號分隔列表。
Cache-Control: no-cache
Cache-Control: no-store
等同于
Cache-Control: no-cache, no-store
此時可以調用.addHeader()方才來添加頭,而不是覆蓋。
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
httpClient.addInterceptor(new Interceptor() {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request original = chain.request();
// Request customization: add request headers
Request.Builder requestBuilder = original.newBuilder()
.addHeader("Cache-Control", "no-cache")
.addHeader("Cache-Control", "no-store");
Request request = requestBuilder.build();
return chain.proceed(request);
}
});
OkHttpClient client = httpClient.build();
- .header(key,value):如果已經存在由鍵標識的現有頭,則用值覆蓋相應的值
- .addHeader(key,value):添加相應的標題鍵和值,即使存在具有相同鍵的現有標題字段
使用@HeaderMap添加請求頭
Dynamic Request Headers
前邊文章中顯示的方法都是靜態的。 雖然你可以更改請求頭的值,但無法動態選擇實際發送的請求頭。 ,@HeaderMap可以讓你在運行時決定哪些頭被添加到你的請求。
與@Header注釋類似,需要將@HeaderMap聲明為接口參數之一。 參數的類型需要實現Java Map接口:
public interface TaskService {
@GET("/tasks")
Call<List<Task>> getTasks(
@HeaderMap Map<String, String> headers
);
}
使用我們上面聲明的接口非常簡單。 您可以創建一個Map實例,并根據您的需要使用值填充它。 Retrofit將@HeaderMap的每個非空元素添加為請求標頭。
TaskService taskService = ServiceGenerator.createService(TaskService.class);
Map<String, String> map = new HashMap<>();
map.put("Page", String.valueOf(page));
if (BuildConfig.DEBUG) {
map.put("Accept", "application/vnd.yourapi.v1.full+json");
map.put("User-Agent", "Future Studio Debug");
}
else {
map.put("Accept", "application/json");
map.put("Accept-Charset", "utf-8");
map.put("User-Agent", "Future Studio Release");
}
Call<List<Task>> call = taskService.getTasks(map);
// Use it like any other Retrofit call
多個具有相同名稱的查詢參數
Query Parameters
Query parameters 是從客戶端傳遞數據到服務器的最常見的方式。
https://api.example.com/tasks?id=123
上面的例子是通過路由"tasks"傳遞數據"id=123"到服務器。
在Retrofit中可以如下寫
public interface TaskService {
@GET("/tasks")
Call<Task> getTask(@Query("id") long taskId);
}
在getTask方法中需要傳入參數"taskId",Retrofit會自動轉換為"/task?id=<taskid>"形式。
Multiple Query Parameters
一些情況下需要傳遞多個相同名字的參數到服務器,類似于下面的樣子:
https://api.example.com/tasks?id=123&id=124&id=125
此時期望服務器的返回值應該是一個任務列表對應的id是ids=[123,124,125]。
在Retrofit中可以很簡單的做到,只需要傳遞一個List即可。
public interface TaskService {
@GET("/tasks")
Call<List<Task>> getTask(@Query("id") List<Long> taskIds);
}
可選查詢參數
根據API的設計,有時候我們需要傳遞可選的參數到服務器。如果你不想傳遞參數,就傳遞null即可。
service.getTasks(null);
Retrofit在重新編譯請求時會自動忽略null的參數。記住,你不能使用原始數據類型來傳遞null,例如:int,float,long等,你必須使用它們的包裝類:Integer,Float,Long等,這樣編譯器不會報錯:
public interface TaskService {
@GET("/tasks")
Call<List<Task>> getTasks(
@Query("sort") String order,
@Query("page") Integer page);
}
現在你可以為getTasks方法傳遞null參數了。
service.getTasks(null, null);
傳遞URL表單數據
表單請求
在Retrofit添加表單請求只需要添加另外一個注解將會直接將你的請求類型轉換為"application/x-www-form-urlencoded".如下邊的例子:
public interface TaskService {
@FormUrlEncoded
@POST("tasks")
Call<Task> createTask(@Field("title") String title);
}
重要的部分是@FormUrlEncoded這個注解。你不能使用get方法時候添加這個注解,表單的目的是傳遞數據到服務器。
此外,必須使用@Field注釋來與您的請求一起發送的參數。 將所需的鍵放在@Field(“key”)注釋中以定義參數名稱。 此外,將您的值的類型添加為方法參數。 如果不使用String,Retrofit將使用Java的String.valueOf(yourObject)方法創建一個字符串值。
service.createTask("Research Retrofit form encoded requests");
生成的結果是
title=Research+Retrofit+form+encoded+requests
假如需要傳遞多個參數,你只需要繼續添加@Field即可。
Form Encoded Requests Using an Array of Values
在上邊的例子中使用@Field注解可以添加字符串數據。但是如果你想要使用對象而不是字符串類型,Retrofit將會把你的對象轉換為字符串。你也可以使用同一個key傳遞一個字符串數組。
public interface TaskService {
@FormUrlEncoded
@POST("tasks")
Call<List<Task>> createTasks(@Field("title") List<String> titles);
}
現在來看一下如何使用
List<String> titles = new ArrayList<>();
titles.add("Research Retrofit");
titles.add("Retrofit Form Encoded")
service.createTasks(titles);
生成的結果如下:
title=Research+Retrofit&title=Retrofit+Form+Encoded
每個條目都被轉換成了類似于map的鍵值對形式,鍵值對與鍵值對之間用&連接,鍵值對內部用=連接。
Field Options
@Filed注解有一個編碼選項encoded,是布爾值。默認是false。
這個encoded定義的內容是你是否已經為鍵值對編碼為url:
@Field(value = "title", encoded = true) String title
Form-Urlencoded vs. Query Parameter
這兩種傳遞數據的方式有什么不同:
form-urlencoded: POST
query parameter: GET
使用form-urlencoded方式傳遞數據的時候數據存放在請求體內,不會再url中顯示,使用query parameter方式傳遞參數時會在url中顯示,多用于篩選或指定區域數據查詢。
使用FieldMap發送表單數據
What Is @Fieldmap in Retrofit?
假如有多個注解,例如添加查詢參數或者路徑參數,使用給定對象請求數據,創建已經編碼好的請求體。舉一個簡單的例子,你現在想更新程序中用戶的數據,你需要請求一個接口要求是鍵值對形式上傳的。接口接受一個JSON字段,你可以這樣寫:
public interface UserService {
@FormUrlEncoded
@PUT("user")
Call<User> update(
@Field("username") String username,
@Field("name") String name,
@Field("email") String email,
@Field("homepage") String homepage,
@Field("location") String location
);
}
PUT方法需要多個參數,例如usernam,email,homepage等。
缺點:每次我們使用新的數據發送更新的時候,我們必須要提供每一個參數的值,即使他們沒有更改,這樣做很繁瑣。
Retrofit提供了一個解決上述問題的方案:@FieldMap。
Form Encoded Requests Using FieldMap
有時候你只需要為某個用戶更新指定字段,你可以使用Retrofit的@FieldMao。你可以使用java標準的Map格式來添加你的鍵值對。
public interface UserService {
@FormUrlEncoded
@PUT("user")
Call<User> update(@FieldMap Map<String, String> fields);
}
Note:假如你只需要更新你的username字段,那就沒有必要添加username以外的字段,你的請求只包含單個字段。
@FiledMap在應用程序中使用的非常廣泛,但是又一點不好的地方:你不知道哪些字段是允許添加的,你也不知道字段名稱,這就需要額外的文檔來說明。
FieldMap Options
同@Field一樣,@FieldMap也包含一個布爾值encoded,默認是false。
使用方法如下:
@FieldMap(encoded = true) Map<String, String> fields
encode定義了每個鍵值對是否已經被編碼。
來看一個簡單的例子。要更新username字段的值為marcus-poehls,默認情況下會變成username=marcus-poehls,使用編碼后會變成username=marcus%2Dpoehls。
為每個請求添加Query Parameters
你可以通過向OkHttpClient添加一個新的請求攔截器來實現。 攔截實際請求并獲取HttpUrl。 http url是添加查詢參數所必需的,因為它將通過附加查詢參數名稱及其值來更改先前生成的請求網址。
OkHttpClient.Builder httpClient =
new OkHttpClient.Builder();
httpClient.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
HttpUrl originalHttpUrl = original.url();
HttpUrl url = originalHttpUrl.newBuilder()
.addQueryParameter("apikey", "your-actual-api-key")
.build();
// Request customization: add request headers
Request.Builder requestBuilder = original.newBuilder()
.url(url);
Request request = requestBuilder.build();
return chain.proceed(request);
}
});
一旦您擁有HttpUrl對象,就可以基于原始的http url對象創建一個新的構建器。 該構建器將允許您使用.addQueryParameter(key,val)方法添加其他查詢參數。 添加查詢參數后,使用.build()方法創建新的HttpUrl對象,該對象通過使用Request.Builder添加到請求中。 上面的代碼使用基于原始請求的附加參數構建新請求,并且只是將網址與新創建的網址進行交換。
使用@QueryMap添加多個查詢參數
Retrofit允許使用@Query注解來添加查詢參數。假如有多個參數需要傳遞,類似于下面:
public interface NewsService() {
@GET("/news")
Call<List<News>> getNews(
@Query("page") int page,
@Query("order") String order,
@Query("author") String author,
@Query("published_at") Date date,
…
);
}
你可以使用null來傳給getNews方法。但是Retrofit提供了一個更好的方法。
How to Use QueryMap
@QueryMap注解使用Map<String,String>來作為參數,每個非空的key的value都會被添加進去。
public interface NewsService() {
@GET("/news")
Call<List<News>> getNews(
@QueryMap Map<String, String> options
);
}
如果你請求作者是Marcus第二頁的新聞,你可以只添加page和author字段,而沒必要再去添加其他字段。
private void fetchNews() {
Map<String, String> data = new HashMap<>();
data.put("author", "Marcus");
data.put("page", String.valueOf(2));
// simplified call to request the news with already initialized service
Call<List<News>> call = newsService.getNews(data);
call.enqueue(…);
}
最后的結果是
http://your.api.url/news?page=2&author=Marcus
QueryMap Options
同前面的一樣,它也擁有布爾值encoded,默認是false。此處不講解詳細用法了。
怎樣在請求中使用動態URL
Use-Case Scenarios
說明一下這個動態URl是不使用BaseURL的網址。
- 上傳文件:假如你的APP允許用戶上傳自己的圖片,你可能回把它們保存到自己的服務器上。
- 下載文件:文件能在不同于BaseURL的網址保存
How to Use Dynamic Urls
你可使用@Url注解為請求方法添加動態網址:
public interface UserService {
@GET
public Call<ResponseBody> profilePicture(@Url String url);
}
上邊的@GET注解后邊并沒有添加路由參數,它會自己添加@Url注解的參數作為請求地址。
How Urls Resolve Against Your Base Url
Retrofit2使用OkHttp的httpurl解析站點。
來看第一個例子:
Retrofit retrofit = Retrofit.Builder()
.baseUrl("https://your.api.url/");
.build();
UserService service = retrofit.create(UserService.class);
service.profilePicture("https://s3.amazon.com/profile-picture/path");
// request url results in:
// https://s3.amazon.com/profile-picture/path
因為你使用了一個完全的地址(https://s3.amazon.com/profile-picture/path),Retrofit會替換掉BaseUrl然后使用你輸入的地址。
來看第二個例子
Retrofit retrofit = Retrofit.Builder()
.baseUrl("https://your.api.url/");
.build();
UserService service = retrofit.create(UserService.class);
service.profilePicture("profile-picture/path");
// request url results in:
// https://your.api.url/profile-picture/path
這個例子中你添加的參數被拼接到BaseURL后邊。
來看第三個例子
Retrofit retrofit = Retrofit.Builder()
.baseUrl("https://your.api.url/v2/");
.build();
UserService service = retrofit.create(UserService.class);
service.profilePicture("/profile-picture/path");
// request url results in:
// https://your.api.url/profile-picture/path
第二個和第三個例子之間的區別是:我們添加了v2 /到BaseURl,并使用/在路徑前面。 實際上,這將導致相同的最終請求url,因為以開頭的斜杠開頭的端點url將僅附加到基本url的主機url。 當對端點網址使用前導斜杠時,將忽略主機網址后面的所有內容。 您可以通過從您的端點刪除前導/來解決您的問題。