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/HttpLoggingInterceptor L o g g e r : − − &gt; P O S T / u p l o a d H T T P / 1.1 ( 277 − b y t e b o d y ) D / H t t p L o g g i n g I n t e r c e p t o r Logger: --&gt; POST /upload HTTP/1.1 (277-byte body) D/HttpLoggingInterceptor Logger:>POST/uploadHTTP/1.1(277bytebody)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,网络请求会根据生命周期的不同,自动控制网络请求会自行取消。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值