Android中網絡請求框架的封裝-Retrofit+RxJava+OkHttp

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方法

  1. @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);

  1. 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請求

  1. @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將會被編碼,即將中文和特殊字符進行編碼轉換

  1. @FieldMap

上述Post請求有4個請求參數,假如說有更多的請求參數,那麼通過一個一個的參數傳遞就顯得很麻煩而且容易出錯,這個時候就可以用FieldMap

@FormUrlEncoded
@POST(“book/reviews”)
Call addReviews(@FieldMap Map<String, String> fields);

  1. @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相似,所以就不再單獨介紹了。

其他必須知道的事項

  1. 添加自定義的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);
}

  1. 網絡請求日誌

調試網絡請求的時候經常需要關注一下請求參數和返回值,以便判斷和定位問題出在哪裏,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:&gt;POST/uploadHTTP/1.1(277bytebody)D/HttpLoggingInterceptorLogger: --&gt; POST /upload HTTP/1.1 (277-byte body) 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)

  1. 爲某個請求設置完整的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());

}
}

  1. 取消請求

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,網絡請求會根據生命週期的不同,自動控制網絡請求會自行取消。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章