前篇回顾
- Android组件化 —— 基础(一) - 组件化与集成化
- Android组件化 —— 基础(二) - 组件间通讯
- Android组件化 —— 基础(三) - ARouter
前篇,我们了解了ARouter路由的基本功能及其内部原理。至此,我们已完成Android APP组件化架构的搭建,并解决了组件间的通讯问题。
但,正如上述描述,我们仅是解决了Android APP内部组件通讯。实际开发中,通讯不仅涉及Android APP内部,往往还涉及到一些外部使用场景,如:
- 1、接口路由下发,动态页面跳转或功能调起
- 2、外部APP唤起咱们APP的页面或功能
- 3、Web H5 js调用APP的页面或功能
这些场景并不属Android APP独有,iOS、H5、后台接口都涉及在内,需各端相互配合才能完成上述场景的相关功能,那么各端之间统一的通讯规则就显得尤为重要,URL Scheme或许是不二之选。
URL Scheme
一个正常的Url链接是这样的:
https://www.baidu.com?key=hello
它是由scheme协议头、host域名、path路径、query参数4个部分组成
[scheme:][host][path][?query]
无论是Android,还是iOS都支持开发人员为自身APP注册自定义的URL Scheme,便于其它APP与之通讯。
如何设计一个满足我们业务场景需求的URL是我们首先要解决的问题,通过上面3个场景需求的分析,设计的URL无非就是将 “页面跳转” 和 “功能调用” 这两个需求高效或者说显著的表示出来。
ARouter的URL Scheme
前篇,ARouter中有提到其是支持标准URL Scheme跳转的,我们先尝试直接使用ARouter看是否能满足我们的需求,以第2个场景为例,由外部APP唤起user模块的UserMainActivity和isLogin()功能。
- ARouter - 标准的页面跳转URL定义
页面路由:example://www.demo.com/user/UserMainActivity
上面是我定义的跳转UserMainActivity路由,只需在AndroidManifest.xml中注册这个Scheme,再由ARouter完成跳转。
...
// Activity
class AppMainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
handUri();
}
private fun handUri() {
val uri: Uri? = intent.data
if (uri != null) {
// 交给ARouter处理
ARouter.getInstance().build(uri).navigation()
}
}
}
外部,我使用浏览器来唤起我们的APP
跳转APP
测试下:
- ARouter - 标准的功能调用URL定义
功能路由:example://www.demo.com/user/isLogin
可当我按照标准的URL规范去定义功能调用url时,ARouter并不会明白这个url所表达的意义。通过查看源码我们会发现ARouter会将url中path部分作为它自身的路由部分来处理后续逻辑。
final class _ARouter {
...
protected Postcard build(Uri uri) {
if (null == uri || TextUtils.isEmpty(uri.toString())) {
throw new HandlerException(Consts.TAG + "Parameter invalid!");
} else {
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
uri = pService.forUri(uri);
}
//url的path会作为ARouter的path进行处理
return new Postcard(uri.getPath(), extractGroup(uri.getPath()), uri, null);
}
}
}
显然,功能路由的/user/isLogin是无法匹配到路由的,还记得ARouter跨模块功能调用提供的路由是谁的吗?没错Servcie!一个Service下可能存在多个功能,而isLogin()只是其中一个。我们通过路由也仅仅能拿到Service对象,如果想要通过url调到具体的service内部功能,还要编写大量的代码来进行匹配,并且这样的代码维护性是很差的。
因此,我个人不建议直接将URL Scheme交给ARouter来处理,ARouter只需做好本职APP内部通讯即可,对于Url的处理可以由上层造轮子交给它解析后,再由ARouter来完成跳转或者功能调用。
自定义URL Scheme
既然,Url需交给上层轮子来处理,那么对于Url的定义也就无需参考ARouter规范约束,实际开发中Url的定义往往也不会理想化的完全按照ARouter要求来设计,毕竟适合多端协作的Url才是好的Url,下面以我定义的Url格式为例,开始制造这个轮子。
example://www.demo.com/openApp?action={
"action_type":"jump" , // 动作类型 跳转(jump) 或 功能(call)
"page_type":"native" , // 跳转的页面类型 native web rn flutter
"path":"/user/UserMainActivity" , // 实际路由地址
"params":"{ \"key\" : \"value\" }" // 携带参数
}
可以看到[scheme:][host][path]这三部分是固定不变的,涉及业务逻辑都由参数action字段来决定,action主要由四部分组成:
- action_type : 标识路由动作,jump(页面跳转) ,call(功能调起)
- page_type : 当路由动作为jump时,标识页面跳转目标类型,native (App原生页面),web(H5页面),rn(RN页面),flutter(Flutter页面)
- path : 实际的路由地址
- params : 目标页面或功能所需参数
这四部分基本可以满足页面跳转和功能调起需求了,action对应的Bean对象如下:
data class RouterAction(
var action_type: String? = "", // 行为类型: jump (跳转页面) 、 call (调用功能)
var page_type: String? = "", // 页面类型: native (原生页面) 、 web (H5) 、rn(RN)、 flutter(Flutter)
var path: String? = "", // 路由地址: /search/SearchActivity
var params: JsonObject? = null // 路由目标需要的参数: "{ key : value }"
)
自定义页面跳转 Url
OK,有了路由规则,来看看页面跳转现在的路由是什么样?
页面路由:
example://www.demo.com/openApp?action={"action_type":"jump" ,"page_type":"native","path":"/user/UserMainActivity" , "params":"{}" }
有了路由,开始完成文章开篇时提到的三个场景吧!
场景1:接口路由下发,动态页面跳转
这是一个很常见的场景,例如:
订单详情页底部按钮,在不同的订单状态下会显示不同的业务逻辑按钮(申请退款、取消订单、评价、联系客服等),以往这些按钮的显示规则都由移动开发人员写死在客户端代码里,一旦订单业务进行调整,原显示规则发生改变,就需修改客户端代码,诺流程影响过大,甚至需开启强更发版。
现在有了路由,这些显示规则都可搬移至服务端,由后台开发人员动态返回。
下面我将模拟接口返回路由场景,涉及JSON数据如下:
{
"btnBgColor":"#FF0000",
"btnColor":"#FFFFFF",
"btnTxt":"跳转User页面",
"openUrl":"example://www.demo.com/openApp?action\u003d{\"action_type\":\"jump\" ,\"page_type\":\"native\",\"path\":\"/user/UserMainActivity\" , \"params\":{} }"
}
模拟请求代码:
private fun getBtnForNet() {
Thread {
Thread.sleep(1500)
val json = "{\"btnBgColor\":\"#FF0000\",\"btnColor\":\"#FFFFFF\",\"btnTxt\":\"跳转User页面\",\"openUrl\":\"example://www.demo.com/openApp?action\\u003d{\\\"action_type\\\":\\\"jump\\\" ,\\\"page_type\\\":\\\"native\\\",\\\"path\\\":\\\"/user/UserMainActivity\\\" , \\\"params\\\":{} }\"}"
runOnUiThread {
showButton(JsonParser.fromJsonObj(json, ButtonBean::class.java))
}
}.start()
}
private fun showButton(btnBean: ButtonBean) {
val button = Button(this)
btnBean.btnColor?.run {
button.setTextColor(Color.parseColor(this))
}
btnBean.btnBgColor?.run {
button.setBackgroundColor(Color.parseColor(this))
}
button.text = btnBean.btnTxt
button.setOnClickListener {
RouterManager.jumpUrl(this, btnBean.openUrl)
}
findViewById(R.id.layoutParent).addView(button)
}
可以看到,我将路由处理交给了RouterManager的jumpUrl()函数,实际上RouterManager只是我对ARouter API的封装管理类,最终我将URL Scheme的处理交给了前文提到要造的轮子:SchemeHelper
/**
* 路由管理类
* */
object RouterManager {
private val mSchemeHelper by lazy { SchemeHelper() }
/**
* 跳转 Activity
* */
fun goActivity(context: Context?, path: String, bundle: Bundle? = null) {
ARouter.getInstance().build(path)
.with(bundle)
.navigation(context)
}
...
/**
* Scheme 路由跳转
* */
fun jumpUrl(
context: Context,
jumpUrl: String?,
callBefore: ((context: Context, action: RouterAction) -> Unit)? = null,
callAfter: ((context: Context, json: String?) -> Unit)? = null
) {
mSchemeHelper.jumpUrl(context, jumpUrl, callBefore, callAfter)
}
...
}
而SchemeHepler中要做的事想当然是对Url进行解析,拿出参数action中那四部分数据,根据这四部分数据完成具体的页面跳转。
class SchemeHelper {
/**
* Scheme 路由跳转
* */
fun jumpUrl(
context: Context,
jumpUrl: String?,
callBefore: ((context: Context, action: RouterAction) -> Unit)? = null,
callAfter: ((context: Context, json: String?) -> Unit)? = null
) {
try {
XLog.i("jumpUrl:: $jumpUrl")
//校验协议
if (TextUtils.isEmpty(jumpUrl)) return
val schemeUrl = CommRouter.Scheme.run { "$SCHEME$HOST$PATH" }
if (jumpUrl!!.startsWith(schemeUrl)) {
// 解析Action
var actionJson = UrlUtils.getUrlParam(jumpUrl, CommRouter.Scheme.ACTION)
actionJson = URLDecoder.decode(actionJson)
val routerAction = JsonParser.fromJsonObj(actionJson, RouterAction::class.java)
// 分发路由
dispatchAction(context, routerAction, callBefore, callAfter)
}
} catch (e: Exception) {
XLog.e(e)
}
}
/**
* 根据action_type分发路由
* */
private fun dispatchAction(
context: Context,
routerAction: RouterAction,
callBefore: ((context: Context, action: RouterAction) -> Unit)? = null,
callAfter: ((context: Context, json: String?) -> Unit)? = null
) {
if (TextUtils.isEmpty(routerAction.action_type)) return
when (routerAction.action_type) {
CommRouter.Scheme.ACTION_TYPE_JUMP -> {
// 跳转页面
jumpAction(context, routerAction, callBefore, callAfter)
}
CommRouter.Scheme.ACTION_TYPE_CALL -> {
// 调用功能
callAction(context, routerAction, callBefore, callAfter)
}
else -> {
XLog.e("未知行为类型(action_type)::${routerAction.action_type}")
}
}
}
...
}
通过路由分发,我们已将Url解析为路由跳转(jumpAction)和功能调用(callAction)两部分,现在接口下发的是“/user/UserMainActivity”页面跳转路由,这个路由不就是配置在UserMainActivity上的ARouter路由吗,剩下的就交给ARouter吧!
/**
* 跳转页面
* */
private fun jumpAction(
context: Context,
routerAction: RouterAction,
callBefore: ((context: Context, action: RouterAction) -> Unit)? = null,
callAfter: ((context: Context, json: String) -> Unit)? = null
) {
if (TextUtils.isEmpty(routerAction.page_type) || TextUtils.isEmpty(routerAction.path)) return
callBefore?.invoke(context, routerAction)
when (routerAction.page_type) {
CommRouter.Scheme.PAGE_TYPE_NATIVE,
CommRouter.Scheme.PAGE_TYPE_WEB -> {
// 原生页面 & H5 处理方式一样 ,都交给ARouter处理
RouterManager.goActivity(
context,
routerAction.path!!,
bundle
)
}
CommRouter.Scheme.PAGE_TYPE_RN -> {
// RN
}
else -> {
XLog.e("未知页面类型(page_type)::${routerAction.page_type}")
}
}
callAfter?.invoke(context, "")
}
来,运行下看看效果:
自定义功能调起 Url
页面跳转完成,接下来看看功能调起的路由又该如何实现。
功能路由:
example://www.demo.com/openApp?action={"action_type":"call" ,"page_type":"","path":"/user/isLogin" , "params":"{}" }
场景1:接口路由下发,动态功能调起
思考下:开篇时,我们提到ARouter是无法直接通过路由去调起一个功能,还需要借助Service来做中转,“/user/isLogin”又属于哪个Service呢,对于User模块的开发者知道这个功能属于他的模块,但对于其他开发人员就不一定了。如果每个业务模块对外提供一个功能,都在SchemeHelper这个轮子里进行编写代码调起自身模块的Service功能,显然是不合适的!怎么办?
还记的 “Android组件化 —— 基础(二) - 组件间通讯” 篇章中我们是如何手动实现路由框架的吗?这里是类似的,对于功能调起的路由需要开发人员手动注册到SchemeHelper中,至于使用哪个Service,调起哪个功能,都不应该由轮子来操心,回调给注册的开发人员去实现即可。
下面是我定义的回调函数接口:
interface IRouterCall {
/**
* @param context 上下文
* @param path 功能路由
* @param bundle 携带来的参数
* @return 该功能可以返回数据,JSON格式字符串
* */
fun handleCall(context: Context, path:String , bundle: Bundle): String?
}
业务开发人员将对外提供的功能函数编写完毕后,再实现一个对应的IRouterCall子类,并将该子类对象与其对应的路由注册到SchemeHelper中。
/**
* User模块对外提供的ARouter Service
* */
@Route(path = "/user/UserService")
class IUserServiceImpl2 : IUserService2 {
override fun init(context: Context?) {
}
/**
* 用户是否登录
* */
override fun isLogin(): Boolean {
// 是否登录业务逻辑
return true
}
}
/**
* /user/isLogin功能路由的处理类
*/
class IsLoginCall : IRouterCall {
override fun handleCall(context: Context, path: String, bundle: Bundle): String? {
val ret = RouterManager.getService(IUserService2::class.java)?.isLogin() ?: false
Toast.makeText(context, "用户登录状态:$ret", Toast.LENGTH_SHORT).show()
return null
}
}
/**
* User模块初始化入口
* */
object UserInit {
fun init(context: Context) {
initRouter()
}
private fun initRouter() {
// 注册功能路由 /user/isLogin
RouterManager.addRouterCall("/user/isLogin" , IsLoginCall())
}
}
与手动实现路由框架时相同,我会将路由路径以及Call对象存储到Map容器中,在路由页面跳转部分,已经将路由处理分发为jumpAction() 和 callAction()函数,这里在callAction()函数里进行匹配路由,再回调给开发人员即可。
object RouterManager {
private val mSchemeHelper by lazy { SchemeHelper() }
...
/**
* 注册自己业务的路由处理器
* */
fun addRouterCall(
path: String,
call: IRouterCall
) {
mSchemeHelper.registerCall(path, call)
}
...
}
class SchemeHelper {
// actionType : call 路由容器
private val mCallGroup = HashMap()
/**
* 注册Call功能
* */
fun registerCall(path: String, call: IRouterCall) {
mCallGroup[path] = call
}
/**
* 调起功能
* */
private fun callAction(
context: Context,
routerAction: RouterAction,
callBefore: ((context: Context, action: RouterAction) -> Unit)? = null,
callAfter: ((context: Context, json: String?) -> Unit)? = null
) {
if (TextUtils.isEmpty(routerAction.path)) return
// 正常触发 call
callBefore?.invoke(context, routerAction)
var resultJson: String? = null
val call = mCallGroup[routerAction.path]
if (call != null) {
// 自定义的Call功能实现
resultJson = call.handleCall(context, routerAction.path!! , bundle)
} else {
when (routerAction.page_type) {
// TODO 公共的Call功能实现
handleCommCall(context, routerAction.path!! , bundle)
}
}
callAfter?.invoke(context, resultJson)
}
}
测试下,试试App模块是否能调起User模块的isLogin功能:
private fun getBtnForNet() {
Thread {
Thread.sleep(1500)
val json = "{\"btnBgColor\":\"#FF0000\",\"btnColor\":\"#FFFFFF\",\"btnTxt\":\"调起isLogin\",\"openUrl\":\"example://www.demo.com/openApp?action\\u003d{\\\"action_type\\\":\\\"call\\\" ,\\\"page_type\\\":\\\"\\\",\\\"path\\\":\\\"/user/isLogin\\\" , \\\"params\\\":{} }\"}"
runOnUiThread {
showButton(JsonParser.fromJsonObj(json, ButtonBean::class.java))
}
}.start()
}
至此,SchemeHelper轮子基本成型,我们已经顺利打通动态的页面跳转和功能调用场景,而至于场景2、场景3的实现,将通讯数据换成Scheme Url,拿到Url后扔给轮子处理即可,我把核心代码贴在下方,就不分别演示了。
场景2:外部APP唤起咱们APP的页面或功能
- 清单文件配置Scheme:
...
...
- Activity中获取Url,并交给轮子处理
@Route(path = "/app/AppMainActivity")
class AppMainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
handUri();
}
private fun handUri() {
RouterManager.jumpUrl(this, intent?.dataString)
}
}
场景3:Web H5 Js调用APP原生页面或功能
- webView暴露JS接口
mWebView.addJavascriptInterface(JsApi(mContext), “androidJs”)
/**
* 供H5调用的Js接口
* */
class JsApi(private val mContext: Context) {
/**
* 通过路由调起APP的页面或功能
* */
fun openApp(openUrl: String) {
RouterManager.jumpUrl(mContext , openUrl)
}
}
- H5 Js调用
window.androidJs.openApp("example://www.demo.com/openApp?action={\"action_type\":\"call\" ,\"page_type\":\"\",\"path\":\"/user/isLogin\" , \"params\":\"{}\" }")
小结
本篇,我们学习了URL Scheme在组件化场景中的使用,它为多场景开发中通讯提供了统一标准,使业务实现更加灵活,一个设计完善的Url路由可使开发人员一眼就知其作用,从而降低代码维护成本;试想下在场景1和场景3中,如果不使用Url路由来做通讯,客户端开发人员就不得编写大量的代码来完善这些功能,而这些功能涉及到流程变动时往往伴随着发版,使用路由做通讯可使发版频次降低。
虽然我们完成了Url路由的通讯功能,但在处理Call路由时,还是采取了手动注册的方式。通过前两篇学习,我们知道手动注册会在APP启动时通过startup来完成,这个路由可能并未被使用就已被载入内存中,导致额外内存开销。在学习ARouter过程中,发现其是通过APT(注解处理器)方案来完成注册相关工作。
下篇,不妨参考ARouter的APT实现,编写一个完成Call路由注册的注解处理器。那么,我们下篇再见~