安卓
Android 的技术日新月异,本文只是提供一些思路,仅供参考。
回调线程切换
在 Android 开发中,经常会把某些代码放到特点的线程去执行,比如网络请求响应后的页面更新在主线程(UI线程)执行,而保存文件则在IO线程操作。OkHttps 为这类问题提供了良好的方案。
在 默认 情况下,所有回调 函数都会 在 IO 线程 执行。为什么会设计如此呢?这是因为 OkHttps 只是纯粹的 Java 领域 Http工具包,本身对 Android 不会有任何依赖,因此也不知 Android 的 UI 线程为何物。这么设计也让它在 Android 之外有更多的可能性。
配置
但是在 Android 里使用 OkHttps 的话,UI线程的问题能否优雅的解决呢?当然可以!简单粗暴的方法就是配置一个 回调执行器:
HTTP http = HTTP.builder()
.callbackExecutor((Runnable run) -> {
// 实际编码中可以吧 Handler 提出来,不需要每次执行回调都重新创建
new Handler(Looper.getMainLooper()).post(run); // 在主线程执行
})
.build();
2
3
4
5
6
用例
上述代码便实现了让 所有 的 回调函数 都在 主线程(UI线程) 执行的目的,如:
http.async("/users")
.addBodyPara("name", "Jack")
.setOnProcess((Process process) -> {
// 在主线程执行
})
.setOnResponse((HttpResult result) -> {
// 在主线程执行
})
.setOnException((Exception e) -> {
// 在主线程执行
})
.setOnComplete((State state) -> {
// 在主线程执行
})
.post();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
但是,如果同时还想让某些回调放在IO线程,实现 自由切换,怎么办呢?OkHttps 给出了非常灵活的方法,如下:
http.async("/users")
.addBodyPara("name", "Jack")
.setOnProcess((Process process) -> {
// 在主线程执行
})
.nextOnIO() // 指定下一个回调在 IO 线程执行
.setOnResponse((HttpResult result) -> {
// 在 IO 线程执行
})
.setOnException((Exception e) -> {
// 在主线程执行(没有指明 nextOnIO 则在回调执行器里执行)
})
.nextOnIO() // 指定下一个回调在 IO 线程执行
.setOnComplete((State state) -> {
// 在 IO 线程执行
})
.post();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
无论是哪一个回调,都可以使用nextOnIO()
方法自由切换。同样,对于文件下载也是一样:
http.sync("/download/test.zip")
.get()
.getBody()
.setOnProcess((Process process) -> {
// 在主线程执行
})
.toFolder("D:/download/")
.nextOnIO() // 指定下一个回调在 IO 线程执行
.setOnSuccess((File file) -> {
// 在 IO 线程执行
})
.setOnFailure((Failure failure) -> {
// 在主线程执行
})
.start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
生命周期绑定
由于 OkHttps 并不依赖于 Android,所以它并没有提供关于生命周期绑定的直接实现,但它的一些扩展机制让我们很容易就可以实现这个需求。在开始之前,我们首先要理解何为生命周期绑定:
HTTP 请求的生命周期绑定
所谓的生命周期绑定:即是让 HTTP 任务感知其所属的 Activity 或 Fragment 的生命周期,当 Activity 或 Fragment 将被销毁时,框架应自动的把由它们发起的但尚未完成的 HTTP 任务全部取消,以免导致程序出错!
现在我们需要对HTTP
实例进行配置,配置后的HTTP
实例具有生命周期绑定的功能,在Activity
或Fragment
里,它的使用效果如下:
// 在 Activity 或 Fragment 内发起请求
http.async("http://www.baidu.com")
.bind(getLifecycle()) // 绑定生命周期
.setOnResponse((HttpResult result) -> {
Log.i("FirstFragment", "收到请求:" + result.toString());
})
.get();
2
3
4
5
6
7
上述代码中的getLifecycle()
是Activity
或Fragment
自带的方法,而bind()
是HttpTask
的现有方法。在配置好HTTP
实例后,上述代码发起的请求便可以感知Activity
或Fragment
的生命周期。
那HTTP
实例到底该如何配置呢?
第一步:配置预处理器
// 绑定到主线程的 Handler
Handler mainHandler = new Handler(Looper.getMainLooper());
HTTP http = HTTP.builder()
... // 省略其它配置项
.addPreprocessor((Preprocessor.PreChain chain) -> {
HttpTask<?> task = chain.getTask();
Object bound = task.getBound();
// 判断 task 是否绑定了 Lifecycle 对象
if (bound instanceof Lifecycle) {
// 注意这一步要放到主线程中执行
mainHandler.post(() -> {
// 重新绑定一个 生命周期监视器(LCObserver)对象,它的定义见下一步
task.bind(new LCObserver(task, (Lifecycle) bound));
})
}
chain.proceed();
})
... // 省略其它配置项
.build();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
第二步:定义生命周期监视器
public class LCObserver implements LifecycleObserver {
HttpTask<?> task;
Lifecycle lifecycle;
LCObserver(HttpTask<?> task, Lifecycle lifecycle) {
this.task = task;
this.lifecycle = lifecycle;
lifecycle.addObserver(this);
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void onStop() {
task.cancel(); // 在 ON_STOP 事件中,取消对应的 HTTP 任务
}
public void unbind() {
// 在请求完成之后移除监视器
lifecycle.removeObserver(this);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
第三步:配置全局回调监听
以上两步其实已经实现了生命周期绑定的功能,但是在请求完成之后,我们需要在lifecycle
中移除LCObserver
对象:
// 绑定到主线程的 Handler
Handler mainHandler = new Handler(Looper.getMainLooper());
HTTP http = HTTP.builder()
... // 省略其它配置项
.completeListener((HttpTask<?> task, HttpResult.State state) -> {
Object bound = task.getBound();
// 判断 task 是否绑定了生命周期监视器(LCObserver)对象
if (bound instanceof LCObserver) {
// 解绑监视器(放到主线程执行)
mainHandler.post(() -> ((LCObserver) bound).unbind())
}
return true;
})
... // 省略其它配置项
.build();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
总结
上述实现生命周期绑定的过程,关键在于对HttpTask
的bind(Object object)
方法传入一个LifeCycle
对象,然后在预处理器和全局监听里做了相关处理。实际上,我们还可以在调用bind
方法时传入一个Activity
或Fragment
对象,这样在预处理器和全局监听里,就不仅可以得到LifeCycle
,还可以得到Context
对象,可参考下文的 安卓-最佳实践。
提示
以上三步实现的生命周期绑定,不仅对 HTTP 请求有作用,对 WebSocket 连接也同样有效哦。
自动加载框
在调用一个耗时较长的接口时,我们往往需要显示一个加载框,以便让用户知道我们的 APP 正在工作而不是卡死。
一般的做法,是这样实现:
showLoading(); // 在请求开始之前显示加载框
http.async('/api/...')
...
.setOnComplete(state -> {
// 在请求结束(成功|失败|异常)之后关闭加载框
hideLoading();
})
.post();
2
3
4
5
6
7
8
9
这样当然可以解决问题,但是一个应用开发下来至少涉及到有三四十个接口请求,多的甚至到成百上千个,如果每个加载框的逻辑都这么写,会造成很多冗余代码,很不优雅。
那可不可以,在请求接口时,我们只给一个标签,如果有这个标签,请求时就自动显示加载框,结束后就自动关闭加载框呢?像这样:
http.async('/api/...')
.tag("LOADING") // 自动显示和关闭加载框
...
.post();
2
3
4
当然可以!OkHttps 扩展机制的强大,一不小心又体现了。来,我们直接看实现代码!
第一步:配置预处理器
预处理器是在网络请求开始之前执行,我们可以再这里显示加载框:
HTTP http = HTTP.builder()
... // 省略其它配置项
.addPreprocessor(chain -> {
HttpTask<?> task = chain.getTask();
// 根据标签判断是否显示加载框
if (task.isTagged("LOADING")) {
showLoading(context(task)); // 显示加载框
}
chain.proceed();
})
... // 省略其它配置项
.build();
2
3
4
5
6
7
8
9
10
11
12
其中第9行使用了一个context(HttpTask<?> task)
方法,它的作用是从HttpTask
的绑定对象中取得一个Context
对象,用于创建加载框。
第二步:配置全局监听
HTTP http = HTTP.builder()
... // 省略其它配置项
.completeListener((HttpTask<?> task, HttpResult.State state) -> {
if (task.isTagged("LOADING")) {
hideLoading(); // 关闭加载框(视情况是否需要放到主线程执行)
}
return true;
});
... // 省略其它配置项
.build();
2
3
4
5
6
7
8
9
10
总结
以上两步变实现了网络请求时的自动显示与关闭加载框的功能,是不是很简单呢,全部代码可参考下文的 安卓-最佳实践。
最佳实践
本章节,将以源码的方式为你呈现使用 OkHttps 的正确姿势,包括:
- 默认回调线程
- 生命周期绑定
- 加载框自动显示关闭
- TOKEN 自动添加与刷新
- 错误码统一处理与回调阻断
标签常量
由于或多或少需要用到一些标签,我们最好把它定义成常量:
public class Tags {
/**
* 用于标记某接口需要 Token 头信息
* 如果没有办法得到合法的 Token,则跳转登录
*/
public static final String TOKEN = "TOKEN";
/**
* 用于标记某接口是否开启自动加载框功能
*/
public static final String LOADING = "LOADING";
// 若有其它的想法,还可以定义其它的标签
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
接口路径常量
由于同一个接口可能会在多处使用,所以我们可以把它们定义为常量(这一步可选):
public class Urls {
/**
* BaseUrl 还可以根据 build.gradle 的配置来取
* 打出不同环境的包,自动使用不同的 BaseUrl,这里便不再示例
*/
public static final String BASE_URL = "https://api.example.com";
/**
* 当 Token 快过期时,调用该接口来刷新 Token
*/
public static final String TOKEN_REFRESH = "/oauth/access-token";
// 其它接口 ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
配置 OkHttps
由于我们推荐在主应用中直接使用OkHttps
类,所以我们在OkHttpsConfig
中配置,可参考 起步-配置 OkHttps 章节。
主干配置
public class OkHttpsConfig implements Config {
// 绑定到主线程的 Handler
private Handler mainHandler = new Handler(Looper.getMainLooper());
@Override
public void with(HTTP.Builder builder) {
builder.baseUrl(Urls.BASE_URL) // 配置 BaseURL
// 如果默认请求体是JSON,则开启,否则默认为表单
//.bodyType("json")
// 配置默认回调在主线程执行
.callbackExecutor(run -> mainHandler.post(run))
// 加载框预处理(在 TOKEN 处理之前)
.addPreprocessor(chain -> {
HttpTask<?> task = chain.getTask();
// 根据标签判断是否显示加载框
if (task.isTagged(Tags.LOADING)) {
showLoading(context(task));
}
chain.proceed();
})
// 实现生命周期绑定
.addPreprocessor(chain -> {
HttpTask<?> task = chain.getTask();
Object bound = task.getBound();
mainHandler.post(() -> {
// 放到主线程执行
task.bind(new BoundWrapper(task, bound));
});
chain.proceed(); // 注意如果你有同步请求,则这一步不能放到主线程里
})
// TOKEN 处理(串行预处理)
.addSerialPreprocessor(chain -> {
HttpTask<?> task = chain.getTask();
// 根据标签判断是否需要 Token
if (!task.isTagged(Tags.TOKEN)) {
chain.proceed();
return;
}
Context ctx = context(task);
requestTokenAndRefreshIfExpired(ctx, chain.getHttp(),
(String token) -> {
if (token != null) {
// 添加 Token 头信息,名字需要和后端商定
task.addHeader("Access-Token", token);
} else if (ctx != null) {
// 若没有得到 Token, 则跳转登录页面
ctx.startActivity(new Intent(ctx, LoginActivity.class));
} else {
Log.e("OkHttps", "没有 Context 无法跳转登录页面!");
}
// 无论如何,这行代码一定要执行到,不然后续接口会一直在排队中
chain.proceed();
});
})
// 错误码统一处理
.responseListener((HttpTask<?> task, HttpResult result) -> {
// 刷新 Token 的接口可以例外
if (task.getUrl().contains(Urls.TOKEN_REFRESH)
|| result.isSuccessful()) {
// 这里演示的是 HTTP 状态码的处理,如果有自定义的 code, 也可以进行深层次的判断
return true; // 继续接口的业务处理
}
// 向用户展示接口的错误信息(视情况是否需要放到主线程执行)
showMsgToUser(task, result.getBody().toString());
return false; // 阻断
})
// 生命周期绑定:第三步
.completeListener((HttpTask<?> task, HttpResult.State state) -> {
Object bound = task.getBound();
if (bound instanceof BoundWrapper) {
// 放到主线程执行
mainHandler.post(() -> ((BoundWrapper) bound).unbind());
}
// 网络错误统一处理(视情况判断是否需要放到主线程上执行)
switch (state) {
case TIMEOUT:
showMsgToUser(task, "网络连接超时");
break;
case NETWORK_ERROR:
showMsgToUser(task, "网络错误,请检查WIFI或数据是否开启");
break;
case EXCEPTION:
showMsgToUser(task, "接口请求异常: " + task.getUrl());
break;
}
if (task.isTagged(Tags.LOADING)) {
hideLoading(); // 关闭加载框
}
return true;
});
}
// 其它 ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
上述是一个主干配置,其中涉及到showLoading
方法、hideLoading
方法、BoundWrapper
类、context
方法、showMsgToUser
方法 和 requestTokenAndRefreshIfExpired
方法,下面依次给出它们的实现:
showLoading
和hideLoading
方法
public class OkHttpsConfig implements Config {
// 省略其它...
private ProgressDialog loading = null;
private AtomicInteger loadings = new AtomicInteger(0);
// 显示加载框
private void showLoading(Context ctx) {
if (loading == null) {
// 这里就用 ProgressDialog 来演示了,当然可以替换成你喜爱的加载框
loading = new ProgressDialog(ctx);
loading.setMessage("正在加载,请稍等...");
}
// 增加加载框显示计数
loadings.incrementAndGet();
loading.show();
}
// 关闭加载框
private void hideLoading() {
// 判断是否所有显示加载框的接口都已完成
if (loadings.decrementAndGet() <= 0
&& loading != null) {
loading.dismiss();
loading = null;
}
}
// 其它 ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
BoundWrapper
类
它可以单独做为一个类,也可以写成内部类:
public class OkHttpsConfig implements Config {
// 省略其它...
static class BoundWrapper implements LifecycleObserver {
HttpTask<?> task;
Lifecycle lifecycle;
Object bound;
BoundWrapper(HttpTask<?> task, Object bound) {
this.task = task;
if (bound instanceof LifecycleOwner) {
lifecycle = ((LifecycleOwner) bound).getLifecycle();
lifecycle.addObserver(this);
}
this.bound = bound;
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void onStop() {
task.cancel(); // 在 ON_STOP 事件中,取消对应的 HTTP 任务
}
void unbind() {
if (lifecycle != null) {
lifecycle.removeObserver(this);
}
}
}
// 其它 ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
context
方法
public class OkHttpsConfig implements Config {
// 省略其它...
/**
* 获取 Context 对象
**/
private Context context(HttpTask<?> task) {
Object bound = task.getBound();
if (bound instanceof BoundWrapper) {
bound = ((BoundWrapper) bound).bound;
}
if (bound instanceof Context) {
return (Context) bound;
}
if (bound instanceof Fragment) {
return ((Fragment) bound).getActivity();
}
// 还可以添加更多获取 Context 的策略,比如从全局 Application 里取
return null;
}
// 其它 ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
showMsgToUser
方法
public class OkHttpsConfig implements Config {
// 省略其它...
/**
* 向用户展示信息
**/
private void showMsgToUser(HttpTask<?> task, String message) {
// 这里就简单用 Toast 示例一下,有更高级的实现可以替换
Context ctx = context(task);
if (ctx != null) {
mainHandler.post(() -> {
Toast.makeText(ctx, message, Toast.LENGTH_LONG).show();
});
}
}
// 其它 ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
requestTokenAndRefreshIfExpired
方法
public class OkHttpsConfig implements Config {
// 省略其它...
/**
* 获取TOKEN,若过期则刷新(代码中的字符串可以替换为常量)
**/
private void requestTokenAndRefreshIfExpired(Context ctx, HTTP http,
Consumer<String> callback) {
if (ctx == null) {
callback.accept(null);
return;
}
// 这里演示使用 Preference 存储,也可以使用数据库存储
SharedPreferences token = ctx.getSharedPreferences("token",
Context.MODE_PRIVATE);
long now = System.currentTimeMillis();
// 刷新令牌
String refreshToken = token.getString("refreshToken", null);
// 判断有效期可以提前 60 秒,以防在接下来的网络延迟中过期了
if (token.getLong("refreshTokenExpiresAt", 0) < now + 60000
|| refreshToken == null) {
// 刷新令牌已过期,说明长时间未使用,需要重新登录
callback.accept(null);
return;
}
// 访问令牌
String accessToken = token.getString("accessToken", null);
if (token.getLong("accessTokenExpiresAt", 0) > now + 60000
&& accessToken != null) {
// 访问令牌未过期,则回调令牌
callback.accept(accessToken);
return;
}
// 访问令牌已过期,刷新令牌未过期,则调接口刷新当前令牌
http.async(Urls.TOKEN_REFRESH)
.skipPreproc() // 跳过所有预处理器
.addBodyPara("refreshToken", refreshToken)
.setOnResponse(res -> {
if (!res.isSuccessful()) {
callback.accept(null);
return;
}
Mapper mapper = res.getBody().toMapper();
String newRefreshToken = mapper.getString("refreshToken");
String newAccessToken = mapper.getString("accessToken");
int refreshTokenExpiresIn = mapper.getInt("refreshTokenExpiresIn");
int accessTokenExpiresIn = mapper.getInt("accessTokenExpiresIn");
// 因为发生了请求,当前时间已经变化,所有重新获取时间
long now2 = System.currentTimeMillis();
// 保存到 SharedPreferences
token.edit()
.putString("refreshToken", newRefreshToken)
.putString("accessToken", newAccessToken)
.putLong("refreshTokenExpiresAt", refreshTokenExpiresIn * 1000 + now2)
.putLong("accessTokenExpiresAt", accessTokenExpiresIn * 1000 + now2)
.commit();
// 回调令牌
callback.accept(newAccessToken);
})
.setOnException(e -> callback.accept(null))
.post();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
使用效果
在 Activity 或 Fragment 中使用
OkHttps.async(Urls.SOME_URL)
.bind(this) // 绑定(生命周期|Context获取)
.tag(Tags.TOKEN) // 自动添加 TOEKN
.tag(Tags.LOADING) // 自动显示加载框
// 请求参数设置...
.setOnResponse(res -> {
// TODO: 正确响应处理
})
.post();
2
3
4
5
6
7
8
9
TIP
上述中的this
得指向 Activity 或 Fragment 本身,如果是在按钮的点击事件中请求,需要指明为YourActivity.this
或者YourFragment.this
在其它能获得 Context 对象的环境里使用
OkHttps.async(Urls.SOME_URL)
.bind(context) // 绑定(生命周期|Context获取)
.tag(Tags.TOKEN) // 自动添加 TOEKN
.tag(Tags.LOADING) // 自动显示加载框
// 请求参数设置...
.setOnResponse(res -> {
// TODO: 正确响应处理
})
.post();
2
3
4
5
6
7
8
9
版本兼容
由于 OkHttps 是在 JDK8 上构建,所以它默认并不支持 Android 的所有版本,但可以通过以下方法来兼容。
默认兼容 Android 7.0+
OkHttps 默认兼容 Android 7.0 及以上的所有版本。
适配 Android 5.0 ~ 6.0
如果你是在 Android 6.0 及以下 版本的设备上运行,并且使用的是 OkHttps v3.4.2 及以上 版本,则需要在您的项目中额外添加 三个 全类名为 java.util.function.BiConsumer
、java.util.function.Consumer
与 java.util.function.Supplier
的代码文件,内容如下:
// v3.4.2 起需要该文件:java/util/function/BiConsumer.java
package java.util.function;
public interface BiConsumer<T, U> {
void accept(T t, U u);
}
// v3.5.0 起需要该文件:java/util/function/Consumer.java
package java.util.function;
public interface Consumer<T> {
void accept(T t);
}
// v3.5.0 起需要该文件:java/util/function/Supplier.java
package java.util.function;
public interface Supplier<T> {
T get();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
工程如图所示:
适配 Android 2.3 ~ 4.4
如果你是在 Android 4.4 及以下 版本的设备上运行,则还需要手动将 okhttp 的版本降低至 3.12.13:
implementation 'com.squareup.okhttp3:okhttp:3.12.13'