Retrofit註解
請求方法
註解代碼 請求格式
@GET GET請求
@POST POST請求
@DELETE DELETE請求
@HEAD HEAD請求
@OPTIONS OPTIONS請求
@PATCH PATCH請求
請求參數
註解代碼 說明
@Headers 添加請求頭
@Path 替換路徑
@Query 替代參數值,通常是結合get請求的
@FormUrlEncoded 用表單數據提交
@Field 替換參數值,是結合post請求的
Retrofit請求的簡單用法
以官方給出的demo爲例:
public final class SimpleService {
public static final String API_URL = “https://api.github.com”;
public static class Contributor {
public final String login;
public final int contributions;
public Contributor(String login, int contributions) {
this.login = login;
this.contributions = contributions;
}
}
public interface GitHub {
@GET("/repos/{owner}/{repo}/contributors")
Call<List> contributors(
@Path(“owner”) String owner,
@Path(“repo”) String repo);
}
public static void main(String… args) throws IOException {
// Create a very simple REST adapter which points the GitHub API.
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
// Create an instance of our GitHub API interface.
GitHub github = retrofit.create(GitHub.class);
// Create a call instance for looking up Retrofit contributors.
Call<List<Contributor>> call = github.contributors("square", "retrofit");
// Fetch and print a list of the contributors to the library.
List<Contributor> contributors = call.execute().body();
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
}
請求方式
Get方法
- @Query
Get方法請求參數都會以key=value的方式拼接在url後面,Retrofit提供了兩種方式設置請求參數。第一種就是像上文提到的直接在interface中添加@Query註解,還有一種方式是通過Interceptor實現,直接看如何通過Interceptor實現請求參數的添加。
public class CustomInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
HttpUrl httpUrl = request.url().newBuilder()
.addQueryParameter(“token”, “tokenValue”)
.build();
request = request.newBuilder().url(httpUrl).build();
return chain.proceed(request);
}
}
addQueryParameter就是添加請求參數的具體代碼,這種方式比較適用於所有的請求都需要添加的參數,一般現在的網絡請求都會添加token作爲用戶標識,那麼這種方式就比較適合。
創建完成自定義的Interceptor後,還需要在Retrofit創建client處完成添加
addInterceptor(new CustomInterceptor())
1
2. @QueryMap
如果Query參數比較多,那麼可以通過@QueryMap方式將所有的參數集成在一個Map統一傳遞,還以上文中的get請求方法爲例
public interface BlueService {
@GET(“book/search”)
Call getSearchBooks(@QueryMap Map<String, String> options);
}
調用的時候將所有的參數集合在統一的map中即可
Map<String, String> options = new HashMap<>();
map.put(“q”, “小王子”);
map.put(“tag”, null);
map.put(“start”, “0”);
map.put(“count”, “3”);
Call call = mBlueService.getSearchBooks(options);
- Query集合
假如你需要添加相同Key值,但是value卻有多個的情況,一種方式是添加多個@Query參數,還有一種簡便的方式是將所有的value放置在列表中,然後在同一個@Query下完成添加,實例代碼如下:
public interface BlueService {
@GET(“book/search”)
Call getSearchBooks(@Query(“q”) List name);
}
最後得到的url地址爲
https://api.douban.com/v2/book/search?q=leadership&q=beyond feelings
1
4. Query非必填
如果請求參數爲非必填,也就是說即使不傳該參數,服務端也可以正常解析,那麼如何實現呢?其實也很簡單,請求方法定義處還是需要完整的Query註解,某次請求如果不需要傳該參數的話,只需填充null即可。
針對文章開頭提到的get的請求,加入按以下方式調用
Call call = mBlueService.getSearchBooks(“小王子”, null, 0, 3);
1
那麼得到的url地址爲
https://api.douban.com/v2/book/search?q=小王子&start=0&count=3
1
5. @Path
如果請求的相對地址也是需要調用方傳遞,那麼可以使用@Path註解,示例代碼如下:
@GET(“book/{id}”)
Call getBook(@Path(“id”) String id);
1
2
業務方想要在地址後面拼接書籍id,那麼通過Path註解可以在具體的調用場景中動態傳遞,具體的調用方式如下:
Call call = mBlueService.getBook(“1003078”);
1
此時的url地址爲
https://api.douban.com/v2/book/1003078
1
@Path可以用於任何請求方式,包括Post,Put,Delete等等。
Post請求
- @field
Post請求需要把請求參數放置在請求體中,而非拼接在url後面,先來看一個簡單的例子
@FormUrlEncoded
@POST(“book/reviews”)
Call addReviews(@Field(“book”) String bookId, @Field(“title”) String title,
@Field(“content”) String content, @Field(“rating”) String rating);
這裏有幾點需要說明的
@FormUrlEncoded將會自動將請求參數的類型調整爲application/x-www-form-urlencoded,假如content傳遞的參數爲Good Luck,那麼最後得到的請求體就是
content=Good+Luck
1
FormUrlEncoded不能用於Get請求
@Field註解將每一個請求參數都存放至請求體中,還可以添加encoded參數,該參數爲boolean型,具體的用法爲
@Field(value = “book”, encoded = true) String book
1
encoded參數爲false的話,key-value-pair將會被編碼,即將中文和特殊字符進行編碼轉換
- @FieldMap
上述Post請求有4個請求參數,假如說有更多的請求參數,那麼通過一個一個的參數傳遞就顯得很麻煩而且容易出錯,這個時候就可以用FieldMap
@FormUrlEncoded
@POST(“book/reviews”)
Call addReviews(@FieldMap Map<String, String> fields);
- @Body
如果Post請求參數有多個,那麼統一封裝到類中應該會更好,這樣維護起來會非常方便
@FormUrlEncoded
@POST(“book/reviews”)
Call addReviews(@Body Reviews reviews);
public class Reviews {
public String book;
public String title;
public String content;
public String rating;
}
其他請求方式
除了Get和Post請求,Http請求還包括Put,Delete等等,用法和Post相似,所以就不再單獨介紹了。
其他必須知道的事項
- 添加自定義的header
Retrofit提供了兩個方式定義Http請求頭參數:靜態方法和動態方法,靜態方法不能隨不同的請求進行變化,頭部信息在初始化的時候就固定了。而動態方法則必須爲每個請求都要單獨設置
靜態方法
public interface BlueService {
@Headers(“Cache-Control: max-age=640000”)
@GET(“book/search”)
Call getSearchBooks(@Query(“q”) String name,
@Query(“tag”) String tag, @Query(“start”) int start,
@Query(“count”) int count);
}
當然你想添加多個header參數也是可以的,寫法也很簡單
public interface BlueService {
@Headers({
“Accept: application/vnd.yourapi.v1.full+json”,
“User-Agent: Your-App-Name”
})
@GET(“book/search”)
Call getSearchBooks(@Query(“q”) String name,
@Query(“tag”) String tag, @Query(“start”) int start,
@Query(“count”) int count);
}
此外也可以通過Interceptor來定義靜態請求頭
public class RequestInterceptor implements Interceptor {
@Override
public Response intercept(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);
}
}
添加header參數Request提供了兩個方法,一個是header(key, value),另一個是.addHeader(key, value),兩者的區別是,header()如果有重名的將會覆蓋,而addHeader()允許相同key值的header存在
然後在OkHttp創建Client實例時,添加RequestInterceptor即可
private static OkHttpClient getNewClient(){
return new OkHttpClient.Builder()
.addInterceptor(new RequestInterceptor())
.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
.build();
}
動態方法
public interface BlueService {
@GET(“book/search”)
Call getSearchBooks(
@Header(“Content-Range”) String contentRange,
@Query(“q”) String name, @Query(“tag”) String tag,
@Query(“start”) int start, @Query(“count”) int count);
}
- 網絡請求日誌
調試網絡請求的時候經常需要關注一下請求參數和返回值,以便判斷和定位問題出在哪裏,Retrofit官方提供了一個很方便查看日誌的Interceptor,你可以控制你需要的打印信息類型,使用方法也很簡單。
首先需要在build.gradle文件中引入logging-interceptor
implementation ‘com.squareup.okhttp3:logging-interceptor:3.12.1’
1
同上文提到的CustomInterceptor和RequestInterceptor一樣,添加到OkHttpClient創建處即可,完整的示例代碼如下:
private static OkHttpClient getNewClient(){
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
return new OkHttpClient.Builder()
.addInterceptor(new CustomInterceptor())
.addInterceptor(logging)
.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
.build();
}
HttpLoggingInterceptor提供了4中控制打印信息類型的等級,分別是NONE,BASIC,HEADERS,BODY,接下來分別來說一下相應的打印信息類型。
NONE
沒有任何日誌信息
Basic
打印請求類型,URL,請求體大小,返回值狀態以及返回值的大小
D/HttpLoggingInterceptorLogger: <-- HTTP/1.1 200 OK (543ms, -1-byte body)
1
2
Headers
打印返回請求和返回值的頭部信息,請求類型,URL以及返回值狀態碼
<-- 200 OK https://api.douban.com/v2/book/search?q=小王子&start=0&count=3&token=tokenValue (3787ms)
D/OkHttp: Date: Sat, 06 Aug 2016 14:26:03 GMT
D/OkHttp: Content-Type: application/json; charset=utf-8
D/OkHttp: Transfer-Encoding: chunked
D/OkHttp: Connection: keep-alive
D/OkHttp: Keep-Alive: timeout=30
D/OkHttp: Vary: Accept-Encoding
D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
D/OkHttp: Pragma: no-cache
D/OkHttp: Cache-Control: must-revalidate, no-cache, private
D/OkHttp: Set-Cookie: bid=D6UtQR5N9I4; Expires=Sun, 06-Aug-17 14:26:03 GMT; Domain=.douban.com; Path=/
D/OkHttp: X-DOUBAN-NEWBID: D6UtQR5N9I4
D/OkHttp: X-DAE-Node: dis17
D/OkHttp: X-DAE-App: book
D/OkHttp: Server: dae
D/OkHttp: <-- END HTTP
Body
打印請求和返回值的頭部和body信息
<-- 200 OK https://api.douban.com/v2/book/search?q=小王子&tag=&start=0&count=3&token=tokenValue (3583ms)
D/OkHttp: Connection: keep-alive
D/OkHttp: Date: Sat, 06 Aug 2016 14:29:11 GMT
D/OkHttp: Keep-Alive: timeout=30
D/OkHttp: Content-Type: application/json; charset=utf-8
D/OkHttp: Vary: Accept-Encoding
D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
D/OkHttp: Transfer-Encoding: chunked
D/OkHttp: Pragma: no-cache
D/OkHttp: Connection: keep-alive
D/OkHttp: Cache-Control: must-revalidate, no-cache, private
D/OkHttp: Keep-Alive: timeout=30
D/OkHttp: Set-Cookie: bid=ESnahto1_Os; Expires=Sun, 06-Aug-17 14:29:11 GMT; Domain=.douban.com; Path=/
D/OkHttp: Vary: Accept-Encoding
D/OkHttp: X-DOUBAN-NEWBID: ESnahto1_Os
D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
D/OkHttp: X-DAE-Node: dis5
D/OkHttp: Pragma: no-cache
D/OkHttp: X-DAE-App: book
D/OkHttp: Cache-Control: must-revalidate, no-cache, private
D/OkHttp: Server: dae
D/OkHttp: Set-Cookie: bid=5qefVyUZ3KU; Expires=Sun, 06-Aug-17 14:29:11 GMT; Domain=.douban.com; Path=/
D/OkHttp: X-DOUBAN-NEWBID: 5qefVyUZ3KU
D/OkHttp: X-DAE-Node: dis17
D/OkHttp: X-DAE-App: book
D/OkHttp: Server: dae
D/OkHttp: {“count”:3,“start”:0,“total”:778,“books”:[{“rating”:{“max”:10,“numRaters”:202900,“average”:“9.0”,“min”:0},“subtitle”:"",“author”:["[法] 聖埃克蘇佩裏"],“pubdate”:“2003-8”,“tags”:[{“count”:49322,“name”:“小王子”,“title”:“小王子”},{“count”:41381,“name”:“童話”,“title”:“童話”},{“count”:19773,“name”:“聖埃克蘇佩裏”,“title”:“聖埃克蘇佩裏”}
D/OkHttp: <-- END HTTP (13758-byte body)
- 爲某個請求設置完整的URL
假如說你的某一個請求不是以base_url開頭該怎麼辦呢?彆着急,辦法很簡單,看下面這個例子你就懂了
public interface BlueService {
@GET
public Call profilePicture(@Url String url);
}
Retrofit retrofit = Retrofit.Builder()
.baseUrl(“https://your.api.url/”); // baseUrl 中的路徑(baseUrl)必須以 / 結束
.build();
BlueService service = retrofit.create(BlueService.class);
service.profilePicture(“https://s3.amazon.com/profile-picture/path”);
直接用@Url註解的方式傳遞完整的url地址即可。
動態設置BaseUrl官方例子
/**
- This example uses an OkHttp interceptor to change the target hostname dynamically at runtime.
- Typically this would be used to implement client-side load balancing or to use the webserver
- that’s nearest geographically.
*/
public final class DynamicBaseUrl {
public interface Pop {
@GET(“robots.txt”)
Call robots();
}
static final class HostSelectionInterceptor implements Interceptor {
private volatile String host;
public void setHost(String host) {
this.host = host;
}
@Override public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
String host = this.host;
if (host != null) {
HttpUrl newUrl = request.url().newBuilder()
.host(host)
.build();
request = request.newBuilder()
.url(newUrl)
.build();
}
return chain.proceed(request);
}
}
public static void main(String… args) throws IOException {
HostSelectionInterceptor hostSelectionInterceptor = new HostSelectionInterceptor();
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(hostSelectionInterceptor)
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.github.com/")
.callFactory(okHttpClient)
.build();
Pop pop = retrofit.create(Pop.class);
Response<ResponseBody> response1 = pop.robots().execute();
System.out.println("Response from: " + response1.raw().request().url());
System.out.println(response1.body().string());
hostSelectionInterceptor.setHost("www.pepsi.com");
Response<ResponseBody> response2 = pop.robots().execute();
System.out.println("Response from: " + response2.raw().request().url());
System.out.println(response2.body().string());
}
}
- 取消請求
Call提供了cancel方法可以取消請求,前提是該請求還沒有執行
String fileUrl = “http://futurestud.io/test.mp4”;
Call call =
downloadService.downloadFileWithDynamicUrlSync(fileUrl);
call.enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
Log.d(TAG, “request success”);
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
if (call.isCanceled()) {
Log.e(TAG, "request was cancelled");
} else {
Log.e(TAG, "other larger issue, i.e. no network connection?");
}
}
});
}
// 觸發某個動作,例如用戶點擊了取消請求的按鈕
call.cancel();
}
Retrofit在項目中實際使用
封裝特點:
1.支持日誌攔截
2.支持設置全局超時時間
3.支持 RESTful 設計標準設計(全面支持GET、POST、PUT、DELETE等請求方式)
4.支持請求緩存
5.支持設置通用請求頭和請求參數
6.與LifecycleOwner結合,網絡請求可以根據lifecycleOwner生命週期選擇執行請求或者自動取消請求
7.請求路徑如果是全url路徑的話,會覆蓋baseUrl,如請求第三方接口獲取天氣數據或微信登錄授權等
8.其他後期完善
項目中採用了組件化開發,我們把網絡請求封裝成請求庫(如:module_net_retrofit_lib),在網絡請求庫中配置如下:
dependencies {
//自行封裝的依賴庫(根據情況配置)
compileOnly ‘cc.times.lib:core-common:1.1.5’
compileOnly ‘cc.times.lib:core-widget:1.0.13’
compileOnly ‘cc.times.lib:lifecycle:1.0.4’
// 網絡請求框架,項目地址:https://github.com/square/retrofit
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.5.0'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
// 網絡請求框架,項目地址:https://github.com/square/okhttp
api 'com.squareup.okhttp3:okhttp:3.12.1'
api 'com.squareup.okhttp3:logging-interceptor:3.12.1'
// OkHttp3 Cookie 緩存框架,項目地址:https://github.com/franmontiel/PersistentCookieJar
implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1'
// RxJava2,項目地址:https://github.com/ReactiveX/RxJava
implementation "io.reactivex.rxjava2:rxjava:2.2.8"
// json解析框架,項目地址:https://github.com/google/gson
implementation 'com.google.code.gson:gson:2.8.5'
}
網絡配置初始化:在Application中
/**
* 開發環境網絡請求配置
*/
fun debugConfig() {
val httpConfig = HttpConfig.Builder().baseUrl(CommonApi.apiBaseUrl)
// 打印使用http請求日誌
.addInterceptor(ChuckInterceptor(AppUtil.context))
.setLogLevel(HttpLoggingInterceptor.Level.BODY)
// 設置全局超時時間
.connectTimeoutMillis(DEFAULT_CONNECT_TIMEOUT)
.readTimeoutMillis(OTHER_TIME_OUT)
.writeTimeoutMillis(OTHER_TIME_OUT).build()
HttpUtil.initHttpConfig(httpConfig)
}
工具類:
object HttpUtil {
internal lateinit var httpConfig: HttpConfig
fun initHttpConfig(config: HttpConfig) {
httpConfig = config
}
fun get(url: String): GetRequest = GetRequest(url)
fun post(url: String, isJson: Boolean = false): PostRequest = PostRequest(url, isJson)
fun put(url: String, isJson: Boolean = false): PutRequest = PutRequest(url, isJson)
fun delete(url: String): DeleteRequest = DeleteRequest(url)
fun head(url: String): HeadRequest = HeadRequest(url)
fun options(url: String): OptionsRequest = OptionsRequest(url)
fun patch(url: String): PatchRequest = PatchRequest(url)
fun <T> retryRequest(baseCallback: BaseCallback<T>): Disposable? {
return baseCallback.request.execute(baseCallback)
}
}
網絡請求配置工具類:
class HttpConfig(
baseUrl: String,
interceptors: MutableList,
networkInterceptors: MutableList,
private val defaultConnectTimeout: Long,
private val defaultReadTimeout: Long,
private val defaultWriteTimeout: Long,
retryOnConnectionFailure: Boolean,
isUseCookie: Boolean,
isUseCache: Boolean,
logLevel: HttpLoggingInterceptor.Level,
val commonHeaders: ArrayMap<String, String>,
val commonParams: ArrayMap<String, String>,
sslParam: SSLParam,
hostnameVerifier: HostnameVerifier
) {
companion object {
const val LOG_MAX_LENGTH = 10_000
const val CACHE_SIZE = 10 * 1024 * 1024L
const val CACHE_DIR = “okhttp”
}
private val okHttpClient: OkHttpClient
internal val retrofit: Retrofit
internal val httpMethod: HttpMethod
init {
val okHttpClientBuilder = OkHttpClient.Builder()
// 設置超時時間
okHttpClientBuilder.connectTimeout(defaultConnectTimeout, TimeUnit.MILLISECONDS)
okHttpClientBuilder.readTimeout(defaultReadTimeout, TimeUnit.MILLISECONDS)
okHttpClientBuilder.writeTimeout(defaultWriteTimeout, TimeUnit.MILLISECONDS)
// 設置是連接失敗時是否重試
okHttpClientBuilder.retryOnConnectionFailure(retryOnConnectionFailure)
// 添加攔截器
interceptors.forEach { okHttpClientBuilder.addInterceptor(it) }
networkInterceptors.forEach { okHttpClientBuilder.addNetworkInterceptor(it) }
// 設置是否使用Cookie
if (isUseCookie) {
okHttpClientBuilder.cookieJar(
PersistentCookieJar(
SetCookieCache(),
SharedPrefsCookiePersistor(AppUtil.context)
)
)
}
// 設置是否使用Cache
if (isUseCache) {
okHttpClientBuilder.cache(Cache(File(AppUtil.context.cacheDir, CACHE_DIR), CACHE_SIZE))
}
// 設置打印日誌
if (logLevel != HttpLoggingInterceptor.Level.NONE) {
val httpLoggingInterceptor = HttpLoggingInterceptor {
if (it.isEmpty()) {
return@HttpLoggingInterceptor
} else if (it.startsWith("{") && it.endsWith("}")) {
LogUtil.json(it, false)
} else {
if (it.length > LOG_MAX_LENGTH) {
LogUtil.v(it.substring(0, LOG_MAX_LENGTH), false)
} else {
LogUtil.v(it, false)
}
}
}
httpLoggingInterceptor.level = logLevel
okHttpClientBuilder.addInterceptor(httpLoggingInterceptor)
}
// 配置https
okHttpClientBuilder.sslSocketFactory(sslParam.sslSocketFactory, sslParam.trustManager)
okHttpClientBuilder.hostnameVerifier(hostnameVerifier)
okHttpClient = okHttpClientBuilder.build()
retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.callFactory { newCall(it) }
.build()
httpMethod = retrofit.create(HttpMethod::class.java)
}
private fun newCall(request: Request): Call {
// 判斷用戶是否在請求中設置了超時時間,如果設置了移除該Header
// 同時判斷該超時時間是否和設置的通用超時時間是否相同,如果相同,不認爲用戶單爲這個請求設置了單獨的超時時間
val builder = request.newBuilder()
var connectTimeout = 0L
request.header(HttpHeader.HEAD_SINGLE_REQUEST_CONNECT_TIMEOUT)?.let {
val timeout = it.toLong()
if (timeout != defaultConnectTimeout) {
connectTimeout = timeout
}
builder.removeHeader(HttpHeader.HEAD_SINGLE_REQUEST_CONNECT_TIMEOUT)
}
var readTimeout = 0L
request.header(HttpHeader.HEAD_SINGLE_REQUEST_READ_TIMEOUT)?.let {
val timeout = it.toLong()
if (timeout != defaultReadTimeout) {
readTimeout = timeout
}
builder.removeHeader(HttpHeader.HEAD_SINGLE_REQUEST_READ_TIMEOUT)
}
var writeTimeout = 0L
request.header(HttpHeader.HEAD_SINGLE_REQUEST_WRITE_TIMEOUT)?.let {
val timeout = it.toLong()
if (timeout != defaultWriteTimeout) {
writeTimeout = timeout
}
builder.removeHeader(HttpHeader.HEAD_SINGLE_REQUEST_WRITE_TIMEOUT)
}
return if (connectTimeout + readTimeout + writeTimeout > 0L) {
// 超時時間大於0,說明用戶設置了新超時時間,基於原來的okHttpClient構建一個使用新的超時時間的okHttpClient執行網絡請求
okHttpClient.newBuilder()
.connectTimeout(
if (connectTimeout == 0L) defaultConnectTimeout else connectTimeout,
TimeUnit.MILLISECONDS
)
.readTimeout(if (readTimeout == 0L) defaultReadTimeout else readTimeout, TimeUnit.MILLISECONDS)
.writeTimeout(if (writeTimeout == 0L) defaultWriteTimeout else writeTimeout, TimeUnit.MILLISECONDS)
.build()
.newCall(builder.build())
} else {
// 用戶沒有設置超時時間或設置了通用超時時間一樣的超時時間,使用默認的okHttpClient執行網絡請求
okHttpClient.newCall(request)
}
}
/**
* 網絡請求配置構建者
*/
class Builder {
private var baseUrl = ""
private var interceptors: ArrayList<Interceptor> = ArrayList()
private var networkInterceptors: ArrayList<Interceptor> = ArrayList()
private var defaultConnectTimeout = 10_000L
private var defaultReadTimeout = 10_000L
private var defaultWriteTimeout = 10_000L
private var retryOnConnectionFailure = false
private var isUseCookie = false
private var isUseCache = false
private var logLevel = HttpLoggingInterceptor.Level.NONE
private val commonHeaders = ArrayMap<String, String>()
private val commonParams = ArrayMap<String, String>()
private var sslParam: SSLParam = HttpsUtil.getSslSocketFactory()
private var hostnameVerifier: HostnameVerifier = HttpsUtil.UnSafeHostnameVerifier
fun baseUrl(url: String): HttpConfig.Builder {
baseUrl = url
return this
}
fun addInterceptor(interceptor: Interceptor): HttpConfig.Builder {
interceptors.add(interceptor)
return this
}
fun addNetworkInterceptor(interceptor: Interceptor): HttpConfig.Builder {
networkInterceptors.add(interceptor)
return this
}
/**
* 連接超時時間
* @param millis 單位是毫秒(默認10秒)
*/
fun connectTimeoutMillis(millis: Long): HttpConfig.Builder {
if (millis <= 0) {
throw IllegalArgumentException("connect timeout must Greater than 0")
}
defaultConnectTimeout = millis
return this
}
/**
* 讀取超時時間
* @param millis 單位是毫秒(默認10秒)
*/
fun readTimeoutMillis(millis: Long): HttpConfig.Builder {
if (millis <= 0) {
throw IllegalArgumentException("read timeout must Greater than 0")
}
defaultReadTimeout = millis
return this
}
/**
* 寫入超時時間
* @param millis 單位是毫秒(默認10秒)
*/
fun writeTimeoutMillis(millis: Long): HttpConfig.Builder {
if (millis <= 0) {
throw IllegalArgumentException("write timeout must Greater than 0")
}
defaultWriteTimeout = millis
return this
}
/**
* 連接失敗時是否重新進行網絡請求
* @param retryOnConnectionFailure 默認爲false
*/
fun retryOnConnectionFailure(retryOnConnectionFailure: Boolean): HttpConfig.Builder {
this.retryOnConnectionFailure = retryOnConnectionFailure
return this
}
/**
* 是否開啓cookie
* @param isUseCookie 默認爲false
*/
fun useCookie(isUseCookie: Boolean): HttpConfig.Builder {
this.isUseCookie = isUseCookie
return this
}
/**
* 是否使用緩存
* @param isUseCache 默認爲false
*/
fun useCache(isUseCache: Boolean): HttpConfig.Builder {
this.isUseCache = isUseCache
return this
}
/**
* 設置日誌級別,參考[HttpLoggingInterceptor.Level]
* @param level 默認爲[HttpLoggingInterceptor.Level.NONE]
*/
fun setLogLevel(level: HttpLoggingInterceptor.Level): HttpConfig.Builder {
logLevel = level
return this
}
/**
* 設置通用請求header
* @param key header鍵
* @param value header值
*/
fun commonHeader(key: String, value: String): HttpConfig.Builder {
commonHeaders[key] = value
return this
}
/**
* 設置通用請求參數
* @param key 參數鍵
* @param value 參數值
*/
fun commonParam(key: String, value: String): HttpConfig.Builder {
commonParams[key] = value
return this
}
/**
* 配置ssl
* @param param ssl參數,默認不對證書做任何檢查
*/
fun sslSocketFactory(param: SSLParam): HttpConfig.Builder {
sslParam = param
return this
}
/**
* 主機名驗證
* @param verifier 默認允許所有主機名
*/
fun hostnameVerifier(verifier: HostnameVerifier): HttpConfig.Builder {
hostnameVerifier = verifier
return this
}
fun build(): HttpConfig {
return HttpConfig(
baseUrl, interceptors, networkInterceptors, defaultConnectTimeout
, defaultReadTimeout, defaultWriteTimeout, retryOnConnectionFailure, isUseCookie
, isUseCache, logLevel, commonHeaders, commonParams, sslParam, hostnameVerifier
)
}
}
}
網絡請求基類:
abstract class BaseRequest<N : BaseRequest>(protected val url: String) {
companion object {
val userAgent = HttpHeader.getUserAgent()
val MEDIA_TYPE_STREAM = MediaType.parse("application/octet-stream")!!
val MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8")
/**
* 錯誤類型
*/
const val ERROR_NET = -1
const val ERROR_CONNECT = -2
const val ERROR_TIMEOUT = -3
const val ERROR_SERVER = -4
const val ERROR_DATA = -5
const val ERROR_HANDLE = -6
const val ERROR_UNKNOWN = -7
}
// 請求header
protected val headers = ArrayMap<String, String>()
// 請求參數
protected val params = ArrayMap<String, String>()
// 生命週期所有者
var lifecycleOwner: LifecycleOwner? = null
private set
// 是否爲head請求
protected var isHeadRequest = false
@Suppress("UNCHECKED_CAST")
fun header(key: String, value: String): N {
headers[key] = value
return this as N
}
@Suppress("UNCHECKED_CAST")
open fun param(key: String, value: String): N {
params[key] = value
return this as N
}
/**
* 設置實現了LifecycleOwner的子類
* @param owner 實現了LifecycleOwner的子類,非必傳
* 如果設置了該字段,那麼只能在[Lifecycle.State.DESTROYED]之前發起網絡請求,
* 如果在網絡請求的過程中生命週期到了[Lifecycle.State.DESTROYED],將會自動取消執行網絡請求
* 如果不設置該字段,網絡請求會一直進行下去,直到請求完成
*/
@Suppress("UNCHECKED_CAST")
fun attachToLifecycle(owner: LifecycleOwner): N {
lifecycleOwner = owner
return this as N
}
/**
* 連接超時時間
* @param millis 單位是毫秒
*/
@Suppress("UNCHECKED_CAST")
fun connectTimeoutMillis(millis: Long): N {
if (millis <= 0) {
throw IllegalArgumentException("connect timeout must Greater than 0")
}
header(HttpHeader.HEAD_SINGLE_REQUEST_CONNECT_TIMEOUT, millis.toString())
return this as N
}
/**
* 讀取超時時間
* @param millis 單位是毫秒
*/
@Suppress("UNCHECKED_CAST")
fun readTimeoutMillis(millis: Long): N {
if (millis <= 0) {
throw IllegalArgumentException("read timeout must Greater than 0")
}
header(HttpHeader.HEAD_SINGLE_REQUEST_READ_TIMEOUT, millis.toString())
return this as N
}
/**
* 寫入超時時間
* @param millis 單位是毫秒
*/
@Suppress("UNCHECKED_CAST")
fun writeTimeoutMillis(millis: Long): N {
if (millis <= 0) {
throw IllegalArgumentException("write timeout must Greater than 0")
}
header(HttpHeader.HEAD_SINGLE_REQUEST_WRITE_TIMEOUT, millis.toString())
return this as N
}
/**
* 異步執行網絡請求
* @return 用於解除訂閱
*/
open fun <T> execute(callback: BaseCallback<T>): Disposable? {
// 生命週期所有者不爲null且生命週期已經處於銷燬狀態,那麼不執行網絡請求
if (lifecycleOwner != null && lifecycleOwner!!.lifecycle.currentState == Lifecycle.State.DESTROYED) {
return null
}
// 如果是head請求,那麼只能使用HeadRequestCallback
if (isHeadRequest) {
if (callback !is HeadRequestCallback) {
throw IllegalArgumentException("Head Request should only use HeadRequestCallback")
}
}
checkHeadersAndParams()
callback.request = this
// 執行網絡請求
val disposable = getRequestMethod(callback)
.map {
if (it.isSuccessful) {
callback.convertResponse(it)
} else {
throw ServerException(it.message())
}
}
.applyScheduler()
.subscribe({
try {
callback.onSuccess(it!!)
} catch (e: Exception) {
LogUtil.printStackTrace(e)
callback.onError(ERROR_HANDLE, ResourcesUtil.getString(R.string.net_retrofit_error_handle))
} finally {
callback.onComplete()
}
}, {
try {
LogUtil.printStackTrace(it)
handleRequestError(callback, it as Exception)
} catch (e: Exception) {
LogUtil.printStackTrace(e)
callback.onError(ERROR_HANDLE, ResourcesUtil.getString(R.string.net_retrofit_error_handle))
} finally {
callback.onComplete()
}
})
// 當生命週期所有者不爲null,監聽生命週期變化,如果生命週期走到onDestroy,取消網絡請求
lifecycleOwner?.let { disposable.attachToLifecycle(it) }
return disposable
}
/**
* 自行處理網絡請求
*/
fun execute(): Observable<Response<ResponseBody>> {
checkHeadersAndParams()
return getRequestMethod(null)
}
abstract fun getRequestMethod(callback: BaseCallback<*>?): Observable<Response<ResponseBody>>
protected fun toRequestBody(file: File): RequestBody {
return RequestBody.create(guessMimeType(file.name), file)
}
protected open fun checkHeadersAndParams() {
// 如果用戶沒有設置userAgent,那麼設置默認的userAgent
if (!headers.containsKey(HttpHeader.HEAD_KEY_USER_AGENT)) {
headers[HttpHeader.HEAD_KEY_USER_AGENT] = userAgent
}
// 設置通用請求頭和請求參數
HttpUtil.httpConfig.commonHeaders.entries.forEach { header(it.key, it.value) }
HttpUtil.httpConfig.commonParams.entries.forEach { param(it.key, it.value) }
}
private fun handleRequestError(callback: BaseCallback<*>, e: Exception) {
when (e) {
is UnknownHostException -> callback.onError(
ERROR_NET,
ResourcesUtil.getString(R.string.net_retrofit_error_net)
)
is ConnectException -> callback.onError(
ERROR_CONNECT,
ResourcesUtil.getString(R.string.net_retrofit_error_connect)
)
is SocketTimeoutException -> callback.onError(
ERROR_TIMEOUT,
ResourcesUtil.getString(R.string.net_retrofit_error_timeout)
)
is ServerException -> {
if (e.message == null || e.message!!.isEmpty()) {
callback.onError(ERROR_SERVER, ResourcesUtil.getString(R.string.net_retrofit_error_server))
} else {
callback.onError(ERROR_SERVER, e.message!!)
}
}
is NullPointerException -> callback.onError(
ERROR_DATA,
ResourcesUtil.getString(R.string.net_retrofit_error_data)
)
else -> callback.onError(ERROR_UNKNOWN, ResourcesUtil.getString(R.string.net_retrofit_error_unknown))
}
}
private fun guessMimeType(fileName: String): MediaType {
// 解決文件名中含有#號異常的問題
val name = fileName.replace("#", "")
val fileNameMap = URLConnection.getFileNameMap()
val contentType = fileNameMap.getContentTypeFor(name) ?: return MEDIA_TYPE_STREAM
return MediaType.parse(contentType) ?: return MEDIA_TYPE_STREAM
}
}
POST請求工具類
class PostRequest(url: String, private val isJson: Boolean = false) : BaseRequest(url) {
private val jsonObj = JSONObject()
private var fileParts = ArrayList<MultipartBody.Part>()
override fun param(key: String, value: String): PostRequest {
jsonObj.put(key, value)
return this
}
fun param(key: String, value: Boolean): PostRequest {
jsonObj.put(key, value)
return this
}
fun param(key: String, value: Int): PostRequest {
jsonObj.put(key, value)
return this
}
fun param(key: String, value: Long): PostRequest {
jsonObj.put(key, value)
return this
}
fun param(key: String, value: Float): PostRequest {
jsonObj.put(key, value)
return this
}
fun param(key: String, value: Double): PostRequest {
jsonObj.put(key, value)
return this
}
fun param(key: String, value: JSONObject): PostRequest {
jsonObj.put(key, value)
return this
}
fun param(key: String, value: JSONArray): PostRequest {
jsonObj.put(key, value)
return this
}
fun param(key: String, value: Collection<*>): PostRequest {
jsonObj.put(key, JSONArray(JSONTokener(JsonUtil.toJson(value))))
return this
}
fun param(key: String, value: File): PostRequest {
if (isJson) {
throw IllegalArgumentException("Content-Type is application/json, param can not be file!")
}
fileParts.add(MultipartBody.Part.createFormData(key, value.name, toRequestBody(value)))
return this
}
fun param(key: String, value: List<File>): PostRequest {
if (isJson) {
throw IllegalArgumentException("Content-Type is application/json, param can not be file!")
}
for (item in value) {
fileParts.add(MultipartBody.Part.createFormData(key, item.name, toRequestBody(item)))
}
return this
}
override fun getRequestMethod(callback: BaseCallback<*>?): Observable<Response<ResponseBody>> {
return if (isJson) {
val body = RequestBody.create(MEDIA_TYPE_JSON, jsonObj.toString())
HttpUtil.httpConfig.httpMethod.post(url, headers, ProgressRequestBody(body, callback))
} else {
val builder = MultipartBody.Builder()
if (jsonObj.length() + fileParts.size == 0) {
// 如果沒有一個表單項都沒有,則增加一個空字符串表單項
builder.addFormDataPart("", "")
} else {
val keys = jsonObj.keys()
for (key in keys) {
builder.addFormDataPart(key, jsonObj.get(key).toString())
}
fileParts.forEachByIndex { builder.addPart(it) }
}
val body = builder.setType(MultipartBody.FORM).build()
builder.setType(MultipartBody.FORM)
HttpUtil.httpConfig.httpMethod.post(url, headers, ProgressRequestBody(body, callback))
}
}
GET請求工具類:(PUT、DELETE、PATCH請求類似)
class GetRequest(url: String) : BaseRequest(url) {
override fun getRequestMethod(callback: BaseCallback<*>?): Observable<Response<ResponseBody>> {
return HttpUtil.httpConfig.httpMethod.get(url, headers, params)
}
}
網絡請求回調類,根據服務器的返回數據不同(實體類、數組、字符串等分別封裝),根據項目需求,同時可以在
CZBaseCallback中添加token過期是否重新請求等功能。
/**
-
網絡請求回調,返回數據爲實體類
**/
abstract class CZObjectCallback(private val clazz: Class, isHandleErrorSelf: Boolean = false) : CZBaseCallback(isHandleErrorSelf) {override fun onSuccess(data: String) {
val responseData = JSONObject(data)
val code = responseData.getInt(“code”)
val message = responseData.getString(“msg”)if (code == 0) { val disposable = Observable.just(responseData) .map { it.getJSONObject("data").toString() } .map { JsonUtil.parseObject(it, clazz)!! } .applyScheduler() .subscribe( { success(it) }, { LogUtil.printStackTrace(it) onError(BaseRequest.ERROR_DATA, "") }) request.lifecycleOwner?.let { disposable.attachToLifecycle(it) } } else { handleAsyncRequestError(code, message,this@CZObjectCallback) }
}
abstract fun success(data: T)
}
網絡請求回調基類:
abstract class CZBaseCallback(private val isHandleErrorSelf: Boolean) : StringCallback() {
companion object {
// 是否正在更新token
var isUpdatingToken = false
}
override fun onError(code: Int, message: String) {
super.onError(code, message)
if (code < 0) {
if (code == BaseRequest.ERROR_SERVER) {
error(code, ResourcesUtil.getString(R.string.common_request_error_server))
} else {
error(code, ResourcesUtil.getString(R.string.common_request_error_net))
}
} else {
error(code, message)
}
}
open fun error(code: Int, message: String) {}
protected fun handleAsyncRequestError(code: Int, msg: String, callback: CZBaseCallback) {
if (isHandleErrorSelf) {
// 不需要處理錯誤情況,交給該請求自行處理
onError(code, msg)
return
}
when (code) {
// token過期,刷新token
103 -> updateToken(callback)
// 換手機登錄時可能出現
104 -> LogoutTool.logout()
else -> onError(code, msg)
}
}
/**
* 更新token
*/
private fun updateToken(callback: CZBaseCallback) {
if (isUpdatingToken) {
// 如果已經有請求在更新token,監聽token是否更新
AuthorityManager.addUpdateTokenCallback {
// token更新成功,重新發起請求
HttpTool.retryRequest(callback)
}
return
}
isUpdatingToken = true
RouteUtil.getServiceProvider(ILaunchService::class.java)
?.updateToken()
?.execute(object : CZObjectCallback<LoginEntity>(LoginEntity::class.java, true) {
override fun success(data: LoginEntity) {
AuthorityManager.updateToken(data.token)
isUpdatingToken = false
HttpTool.retryRequest(callback)
}
override fun error(code: Int, message: String) {
super.error(code, message)
isUpdatingToken = false
LogoutTool.logout(desc = ResourcesUtil.getString(R.string.common_account_error))
}
})
}
}
接口調用實例(以登錄爲例):
object LaunchApi {
// 登錄
private const val LOGIN = “user/login”
/**
* 登錄
* @param account 登錄帳號, mobile:手機號,open_id:微信open_id
* @param method 登錄方式,sms:短信登錄, wechat:微信登錄
* @param password 口令, 包括:vcode(驗證碼),token(微信token)
*/
fun login(account: String, method: String, password: String): CZPostRequest {
return HttpTool.post(LOGIN)
.param(“account”, account)
.param(“method”, method)
.param(“passwd”, password)
}
}
在登錄界面調用:
LaunchApi.login(account, method, passwd)
.attachToLifecycle(this)
.execute(object : CZObjectCallback(LoginEntity::class.java) {
override fun success(data: LoginEntity) {
//登錄成功
}
override fun error(code: Int, message: String) {
super.error(code, message)
//登錄失敗
}
})
接口調用說明:在項目中使用了組件化,請求接口LaunchApi中爲啓動組件,該組件中只定義了啓動相關的接口,在請求時,如果添加了attachToLifecycle,網絡請求會根據生命週期的不同,自動控制網絡請求會自行取消。