安卓框架向-行为跳转器

业务场景

UI开发不可避免需要和后端进行交互,部分灵活的页面需要由后端配置进行页面跳转,或者行为触发。

方案一:字段解析,按照对应的开关跳转或者触发不同的行为

字段解析一般是最简单想到的方法,和后台定义携带的参数,像必要的id,是否跳转等等。前端直接解析对应字段,按照必要的逻辑进行处理。

优点

绝大部分都是直接使用定义字段进行处理的,思路比较简单,在开关类场景十分常见,约定了对应的字段进行解析操作

缺点

缺点主要在业务迭代情况下,由开关类的场景转换为内容参数类场景,这时候这种前期定义的参数就会出现很多问题。

如果之前约定的字段是boolean类型,那么可拓展就很难,需要新增boolean类型来做兼容,那么就会出现2X2>3的情况,本来只是多一个场景,却多了两个可能。

如果之前约定的字段是非boolean类型的,那么可拓展比较容易,int类型直接加一个,字符串或者别的类型也可以做变化。

如果之前约定的字段没有,那么需要拓展,需要变更json解析逻辑。

相对来讲如果需要用参数定义,那么最好是非boolean类型的,不过由于业务变更的不确定性,最好同步增加版本号。

方案二:使用Uri做字段定义解析器

这个思路其实是基于方案一的升级

每个需要路由跳转的地方,新增的字段类型是string,内容是编码后的Uri

使用Uri主要是Uri拥有比较完善的解析方式,巧用可以完成像协议,版本号,意图,参数等行为的封装

Uri格式

协议名://[用户名]:[密码]@[服务器地址]:[服务器端口号]/[路径]?[查询字符串]#[片段ID]

完全可以按照这种格式进行处理。

首先是协议名,一般是http,https,files等,可以变成客户端代号,像我在喜马拉雅,安卓项目用的就是tingcar,小程序用的就是miniTingCar,鸿蒙就是hongmengTingCar。这个主要是可以用来区分该协议针对哪个端。

其次是用户名,一般是域名,但是业务上面可以变成协议的版本号,这个版本号可以是大版本的区分,也可不必区分,这个可以业务上面自己做决定

密码/服务器端口号:这个暂时不需要使用

路径:路径可以设计为意图,比较关键,这一次动作的整体行为,比如说跳转哪个页面,执行哪个动作等。

查询关键字/片段id:这里直接添加query,像跳转页面需要的参数,执行动作需要的参数等

客户端需要做的事情

主要是对这个Uri进行安全校验,版本校验,然后意图解析,动作行为执行。

不过有一点,这个动作行为执行需要提前封装。这个需要有一定的意识,在新建页面的时候,对其一些对外的行为就进行封装。

封装的过程不单单局限于参数的传递,还有上下文的获取。这个上下文最好不要做成参数进行传递,而是需要时实时从环境中读取。这样行为封装更为彻底。

优点

1、版本升级,只要约定的协议不变,后续的仍然可以使用该协议进行拓展。

2、携带的参数可以无限制扩大,对于可变参数,例如页面需要走网络请求带的头参,可以使用遍历query一股脑加进去的方式做网络请求。这个是最重要的,甚至可以新建一个页面,把网络请求作为query装进去,然后作为某个页面的传参,直接可以组织成这个页面。

3、由于该协议带来的动作行为封装, 在业务中也十分有用,我观察到绝大部分人有页面跳转封装的思想,但是动作封装的思想却比较少。动作封装其实更加重要,需要兼顾动作的头尾,兼顾整个过程,而页面的封装却十分片面。

缺点

我在喜马拉雅落地了这个,并且使用过程中的确发现了一些问题。

1、和后端需要明确落实,这涉及到一定的沟通向的问题
2、动作行为封装需要比较整合,比如触发的行为是购买,购买前必然要登陆,登陆过程只是这个行为的一次检验,并未确实触发这个行为,需要在登陆完继续完成这个行为
3、版本的升级需要明确哪个版本支持哪些动作,否则运营总是来问,这个最好技术端维护一下文档说明。

实现

1、route过程传入字符串,解析uri,判断协议是否相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 行为触发方法总体
*/
public static void route(@Nullable String router, String traceFrom) {
if (TextUtils.isEmpty(router)) {
ToastUtils.showError("错误!无有效跳转路径", "UriRouter/route: router is null");
return;
}
Uri uriRouter = Uri.parse(router);
if (TINGCAR_SCHEME.equals(uriRouter.getScheme())) {
String path = uriRouter.getPath();
if (path == null) {
ToastUtils.showError("错误!无跳转路径", "UriRouter/route: path is null");
return;
}
switch (path) {
...
}
}
}

2、然后根据意图进行分发

1
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
    switch (path) {
case OPEN_ALBUMFRAGMENT:
openAlbumDetail(uriRouter, traceFrom);
break;
case RECEIVE_ACTIVITY:
receiveActivity(uriRouter, traceFrom);
break;
case OPEN_PLAYLIVEFRAGMENT:
openPlayLiveFragment(uriRouter, traceFrom);
break;
case OPEN_ALBUMLISTFRAGMENT:
openAlbumListFragment(uriRouter, traceFrom);
break;
case OPEN_VIPFRAGMENT:
openVipFragment(uriRouter, traceFrom);
break;
case OPEN_RECOMMENDFRAGMENT:
openRecommendFragment(uriRouter, traceFrom);
break;
case OPEN_COUPONLISTFRAGMENT:
openCouponListFragment(uriRouter, traceFrom);
break;
case OPEN_PLAYRADIOFRAGMENT:
// comment by YinPengcheng: 2020-06-16 4.3版本添加
openPlayRadioFragment(uriRouter, traceFrom);
break;
case OPEN_PLAYTRACKFRAGMENT:
// comment by YinPengcheng: 2020-06-16 4.3版本添加
openPlayTrackFragment(uriRouter, traceFrom);
break;
case OPEN_H5FRAGMENT:
// comment by YinPengcheng: 2020-06-17 4.3版本添加
openH5Fragment(uriRouter, traceFrom);
break;
case BUY_ALBUM:
// comment by YinPengcheng: 2020-06-17 4.3版本添加
buyAlbum(uriRouter, traceFrom);
break;
case BUY_VIP:
// comment by YinPengcheng: 2020-06-17 4.3版本添加
buyVip(uriRouter, traceFrom);
break;
case OPEN_BUY_VIP:
openBuyVip(uriRouter, traceFrom);//4.6新增 非会员->点击弹出连会员购买弹框 已登录是会员->点击弹出会员购买弹框
break;
case OPEN_MINEFRAGMENT:
// comment by YinPengcheng: 2020-06-18 4.3版本添加
openMineFragment(uriRouter, traceFrom);
break;
default:
// TODO: 2020-05-09 发现无法识别的跳转路径,此时最好上报一下服务器,告知版本号之类的
Log.i(TAG, "UriRouter,route: can not resolve " + router + CarOsUtil
.getVersionSnapshot());
break;
}
}

3、分发到行为方法内部,根据方法版本号等参数,决定是否响应该次动作触发,以及埋点等。