Android中网络请求框架的封装-Retrofit+RxJava+OkHttp

前言

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…

  • 请求速度快,使用非常方便灵活

  • ……

    流程图如下:

    Ajo7Zj.md.png

Retrofit注解

  • 请求方法
注解代码请求格式
@GETGET请求
@POSTPOST请求
@DELETEDELETE请求
@HEADHEAD请求
@OPTIONSOPTIONS请求
@PATCHPATCH请求
  • 请求参数
注解代码说明
@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());
  }
}

小结:
根据不同BaseUrl创建不同的Retrofit对象,有点浪费资源(不可取)。

@GET、@POST、@Url不仅可以传相对路径,也可以传绝对路径。

在项目实际使用中:当设置好一个默认的BaseUrl后,其他baseUrl如有更改,直接传全路径Url。

了解更多可参考:
解决Retrofit多BaseUrl及运行时动态改变BaseUrl?

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的源码和原理,为后期的演变打下基础。

参考资料:

1.Android网络请求心路历程

2.Retrofit分析-漂亮的解耦套路

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值