文章目录
前言
okGo项目由于没有维护,性能有点跟不上。现在的主流网络请求框架基本上都选用的是retrofit
公司项目原来使用的是okGo网络请求框架,后来全部替换为retrofit。
本文重点介绍一下retrofit的封装与项目中实际使用。
Retrofit是什么?
官方文档介绍:
Type-safe HTTP client for Android and Java by Square, Inc.
Retrofit 是一个遵循 RESTful 设计标准的一个网络请求封装库。
Retrofit 使用了大量的设计模式,其中动态代理 + 注解的思路来声明后端接口非常优雅,再加上提供网络请求适配器及数据转换器的扩展,基本上已满足大部分的业务需求了。
Retrofit是Square公司出品的默认基于OkHttp封装的一套RESTful网络请求框架,RESTful是目前流行的一套api设计的风格, 并不是标准。Retrofit的封装可以说是很强大,里面涉及到一堆的设计模式,可以通过注解直接配置请求,可以使用不同的http客户端,虽然默认是用http ,可以使用不同Json Converter 来序列化数据,同时提供对RxJava的支持,使用Retrofit + OkHttp + RxJava + Dagger2 可以说是目前比较潮的一套框架,但是需要有比较高的门槛。
注:有关RESTful API设计规范文档请 从点击这里获取
(也可私聊我单独发给你)
Retrofit的好处?
-
超级解耦
-
可以配置不同HttpClient来实现网络请求,如OkHttp、HttpClient…
-
支持同步、异步和RxJava
-
可以配置不同的反序列化工具来解析数据,如json、xml…
-
请求速度快,使用非常方便灵活
-
……
流程图如下:
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<Contributor>> 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())
2. @QueryMap
如果Query参数比较多,那么可以通过@QueryMap方式将所有的参数集成在一个Map统一传递,还以上文中的get请求方法为例
public interface BlueService {
@GET("book/search")
Call<BookSearchResponse> 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<BookSearchResponse> call = mBlueService.getSearchBooks(options);
3. Query集合
假如你需要添加相同Key值,但是value却有多个的情况,一种方式是添加多个@Query参数,还有一种简便的方式是将所有的value放置在列表中,然后在同一个@Query下完成添加,实例代码如下:
public interface BlueService {
@GET("book/search")
Call<BookSearchResponse> getSearchBooks(@Query("q") List<String> name);
}
最后得到的url地址为
https://api.douban.com/v2/book/search?q=leadership&q=beyond%20feelings
4. Query非必填
如果请求参数为非必填,也就是说即使不传该参数,服务端也可以正常解析,那么如何实现呢?其实也很简单,请求方法定义处还是需要完整的Query注解,某次请求如果不需要传该参数的话,只需填充null即可。
针对文章开头提到的get的请求,加入按以下方式调用
Call<BookSearchResponse> call = mBlueService.getSearchBooks("小王子", null, 0, 3);
那么得到的url地址为
https://api.douban.com/v2/book/search?q=%E5%B0%8F%E7%8E%8B%E5%AD%90&start=0&count=3
5. @Path
如果请求的相对地址也是需要调用方传递,那么可以使用@Path注解,示例代码如下:
@GET("book/{id}")
Call<BookResponse> getBook(@Path("id") String id);
业务方想要在地址后面拼接书籍id,那么通过Path注解可以在具体的调用场景中动态传递,具体的调用方式如下:
Call<BookResponse> call = mBlueService.getBook("1003078");
此时的url地址为
https://api.douban.com/v2/book/1003078
@Path可以用于任何请求方式,包括Post,Put,Delete等等。
Post请求
1. @field
Post请求需要把请求参数放置在请求体中,而非拼接在url后面,先来看一个简单的例子
@FormUrlEncoded
@POST("book/reviews")
Call<String> 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
FormUrlEncoded不能用于Get请求
-
@Field注解将每一个请求参数都存放至请求体中,还可以添加encoded参数,该参数为boolean型,具体的用法为
@Field(value = "book", encoded = true) String book
encoded参数为false的话,key-value-pair将会被编码,即将中文和特殊字符进行编码转换
2. @FieldMap
上述Post请求有4个请求参数,假如说有更多的请求参数,那么通过一个一个的参数传递就显得很麻烦而且容易出错,这个时候就可以用FieldMap
@FormUrlEncoded
@POST("book/reviews")
Call<String> addReviews(@FieldMap Map<String, String> fields);
3. @Body
如果Post请求参数有多个,那么统一封装到类中应该会更好,这样维护起来会非常方便
@FormUrlEncoded
@POST("book/reviews")
Call<String> 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<BookSearchResponse> 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<BookSearchResponse> 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<BookSearchResponse> getSearchBooks( @Header("Content-Range") String contentRange, @Query("q") String name, @Query("tag") String tag, @Query("start") int start, @Query("count") int count); }
2. 网络请求日志
调试网络请求的时候经常需要关注一下请求参数和返回值,以便判断和定位问题出在哪里,Retrofit官方提供了一个很方便查看日志的Interceptor,你可以控制你需要的打印信息类型,使用方法也很简单。
首先需要在build.gradle文件中引入logging-interceptor
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.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/HttpLoggingInterceptor$Logger: --> POST /upload HTTP/1.1 (277-byte body) D/HttpLoggingInterceptor$Logger: <-- HTTP/1.1 200 OK (543ms, -1-byte body)
-
Headers
打印返回请求和返回值的头部信息,请求类型,URL以及返回值状态码
<-- 200 OK https://api.douban.com/v2/book/search?q=%E5%B0%8F%E7%8E%8B%E5%AD%90&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=%E5%B0%8F%E7%8E%8B%E5%AD%90&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)
3. 为某个请求设置完整的URL
假如说你的某一个请求不是以base_url
开头该怎么办呢?别着急,办法很简单,看下面这个例子你就懂了
public interface BlueService {
@GET
public Call<ResponseBody> 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<ResponseBody> 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());
}
}
4. 取消请求
Call提供了cancel方法可以取消请求,前提是该请求还没有执行
String fileUrl = "http://futurestud.io/test.mp4";
Call<ResponseBody> call =
downloadService.downloadFileWithDynamicUrlSync(fileUrl);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> 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<Interceptor>,
networkInterceptors: MutableList<Interceptor>,
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<N>>(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<PostRequest>(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<GetRequest>(url) {
override fun getRequestMethod(callback: BaseCallback<*>?): Observable<Response<ResponseBody>> {
return HttpUtil.httpConfig.httpMethod.get(url, headers, params)
}
}
网络请求回调类,根据服务器的返回数据不同(实体类、数组、字符串等分别封装),根据项目需求,同时可以在
CZBaseCallback中添加token过期是否重新请求等功能。
/**
* 网络请求回调,返回数据为实体类
**/
abstract class CZObjectCallback<T>(private val clazz: Class<T>, 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>(LoginEntity::class.java) {
override fun success(data: LoginEntity) {
//登录成功
}
override fun error(code: Int, message: String) {
super.error(code, message)
//登录失败
}
})
接口调用说明:在项目中使用了组件化,请求接口LaunchApi中为启动组件,该组件中只定义了启动相关的接口,在请求时,如果添加了attachToLifecycle,网络请求会根据生命周期的不同,自动控制网络请求会自行取消。
总结
在Android开发中,网络请求框架的优化随着时间变化或需求变化会不断的演变,性能也会逐步得以优化,在使用过程中需要进一步研究Retrofit的源码和原理,为后期的演变打下基础。
参考资料: