# 发送网络请求

APP的联网能力现在必不可少, 所以选择一个适合的网络框架至关重要. 我主要以以下几个方面考虑:

* 使用简单简洁
* 方便的自定义, 如加请求头
* 方便的取消请求
* 能方便的与GSON等序列化库结合
* 完善的Log信息打印

## Retrofit2

最后选择了Retrofit2, 具体介绍可以参考 [用 Retrofit 2 简化 HTTP 请求](https://realm.io/cn/news/droidcon-jake-wharton-simple-http-retrofit-2/). 基本用法可以参考 [官方网站](http://square.github.io/retrofit/).

这里就不再赘述Retrofit2的优缺点, 主要谈谈引入Retrofit2后需要做的一些准备工作:

1. 将Retrofit2的API应用至Model层
2. 与GSON结合
3. 自定义Log输出
4. 处理一些通用的异常
5. 取消请求
6. 预处理请求结果

### 1. 将Retrofit2的API应用至Model层

Retrofit的特色是使用注解与接口来描述网络请求, 在多数情况下能很好的与Model层的接口结合起来, 而无需重写实现类.

```
public interface HomepageApi {
    @POST(Urls.GET_TYPES)
    Call<BaseData> getTypes();

    @FormUrlEncoded
    @POST(Urls.GET_HOT_PRODUCTS)
    Call<BaseData> getTopProducts(@Field("pageNo") int pageNo, 
                      @Field("pageSize") int pageSize);
}
```

```
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl(Urls.BaseUrl)
    .build();

HomepageApi api = retrofit.create(HomepageApi.class);
```

然后通过api对象去调用getTypes或getTopProducts接口. 通过这种方式我们就能直接将描述请求的接口应当做一个Model来使用. 但是每次使用都去要生成一个Retrofit.Builder, 然后做一些配置, 额外增加了重复代码, 因此我们可以写一个工具类, 来专门生成Retrofit接口实例

在这里我使用了两个类:

1. RetrofitFactory 持有一个Retrofit单例, 专门用于配置Retrofit, 比如baseUrl, converter, adapter之类.
2. ApiFactory 单例, 持有一个HashMap对象, 用于缓存Retrofit生成的接口实例, 而无需重复生成.

```
/**
 * 用于获取配置好的retrofit对象, 通过设置{@link Configuration#enableLoggingNetworkParams()}来启用网络请求
 * 参数与相应结果.
 */
public class RetrofitFactory {
    private static Retrofit retrofit;
    private static String baseUrl;

    public static void setBaseUrl(String url) {
        baseUrl = url;
    }

    /**
     * 获取配置好的retrofit对象来生产Api对象
     */
    public static Retrofit getRetrofit() {
        if (retrofit == null) {
            if (baseUrl == null || baseUrl.length() <= 0)
                throw new IllegalStateException("请在调用getFactory之前先调用setBaseUrl");

            Retrofit.Builder builder = new Retrofit.Builder();

            builder.baseUrl(baseUrl)
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 参考RxJava
                    .addConverterFactory(GsonConverterFactory.create()); // 参考与GSON的结合

            // 参考自定义Log输出
            if (Configuration.isShowNetworkParams()) {
                OkHttpClient client = new OkHttpClient.Builder().addInterceptor(
                        new HttpLoggingInterceptor()).build();

                builder.client(client);
            }

            retrofit = builder.build();
        }

        return retrofit;
    }
}
```

```
/**
 * 通过定义好的api接口以及Retrofit来生成具体的实例.
 */
public class ApiFactory {
    private static ApiFactory factory;
    private static HashMap<String, Object> serviceMap = new HashMap<>();

    public static ApiFactory getFactory() {
        if (factory == null) {
            synchronized (ApiFactory.class) {
                if (factory == null)
                    factory = new ApiFactory();
            }
        }
        return factory;
    }

    public <T> T create(Class<T> clz) {
        Object service = serviceMap.get(clz.getName());
        if (service == null) {
            service = RetrofitFactory.getRetrofit().create(clz);
            serviceMap.put(clz.getName(), service);
        }
        return (T) service;
    }
}
```

```
HomepageApi api = ApiFactory.getFactory().create(HomepageApi.class);
```

在使用前还需要调用

```
RetrofitFactory.setBaseUrl(Urls.ROOT);
```

### 2. 与GSON结合

Retrofit2可以很方便的通过Converter与GSON结合, 在`RetrofitFactory`类中已经做了绑定, 无需再处理.

```
builder.addConverterFactory(GsonConverterFactory.create());
```

我们只需要在定义接口的时候给定结果类型, Retrofit内部会调用GSON解析JSON数据, 并转换成相应的实体类.

```
Call<BaseData> getTypes();
```

### 3. 自定义Log输出

为何要有Log输出? 有了网络请求的Log我们就能很方便的追踪问题, 看是服务器返回的数据有误, 还是我们解析有误. Retrofit2内部使用的是okhttp3, 因此可以考虑使用okhttp的Interceptor来打印Log输出:

```
/**
 * OkHttp的{@link Interceptor}, 通过设置
 * {@link Configuration#enableLoggingNetworkParams()}打印网络请求参数与响应结果
 */
public class HttpLoggingInterceptor implements Interceptor {
    private static final String TAG = "HttpLogging";

    @Override
    public Response intercept(Chain chain) throws IOException {

        Request request = chain.request(); // 获取请求
        long t1 = System.nanoTime();

        Buffer buffer = new Buffer();
        request.body().writeTo(buffer); //获取请求体

        Log.e(TAG, String.format("Sending request %s on %s%n%sRequest Params: %s",
                request.url(), chain.connection(), request.headers(), buffer.clone().readUtf8()));
        buffer.close();

        Response response = chain.proceed(request); //执行请求
        long t2 = System.nanoTime();

        BufferedSource source = response.body().source(); //获取请求结果
        source.request(Long.MAX_VALUE);
        buffer = source.buffer().clone(); //克隆返回结果, 因为buffer只能使用一次
        Log.e(TAG, String.format("Received response for %s in %.1fms%n%sResponse Json: %s",
                response.request().url(), (t2 - t1) / 1e6d, response.headers(),
                buffer.readUtf8()));

        return response;
    }
}
```

然后在`RetrofitFactory`中加入:

```
if (Configuration.isShowNetworkParams()) {
    OkHttpClient client = new OkHttpClient.Builder().addInterceptor(
            new HttpLoggingInterceptor()).build();

    builder.client(client);
}
```

以下是Log格式:

```
HttpLogging: Sending request http://xx.xx.xx.xx:xxxx/xxx/app/findAttractions.htm on null
         Request Params: city=%E6%AD%A6%E6%B1%89&pageNo=1&pageSize=10&latitude=xxx&longitude=xxx
```

```
HttpLogging: Received response for http://xx.xx.xx.xx:xxxx/xxx/app/findAttractions.htm in 181.2ms
         Server: Apache-Coyote/1.1
         Content-Type: text/html;charset=UTF-8
         Transfer-Encoding: chunked
         Date: Wed, 23 Mar 2016 06:50:48 GMT
         OkHttp-Sent-Millis: 1458715847034
         OkHttp-Received-Millis: 1458715847128
         Response Json: {"data":{"attractionsList":[{"distance":13110.6,"id":13,"image":"xx.xx.xx.xx:xxxx/xxx/upload/201603171607138491.jpg","latitude":30.559024,"longitude":114.30341,"name":"黄鹤楼","profile":null,"score":5,"voice":"xx.xx.xx.xx:xxxx/xxx/upload/201603171009165109.mp3"}]},"msg":"成功","page":{"pageCount":1,"pageNo":1,"pageSize":10,"totalCount":1},"status":1}
```

### 接下来的几部分都建议在有一定RxJava基础的情况下阅读. 参考[RxJava](/android-architecture-journey/corlibsdesign_md/rxjava.md).


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ryan-8.gitbook.io/android-architecture-journey/corlibsdesign_md/request_network.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
