本文为稀土技能社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

前言

假如用过 ARouter 做组件化开发,或许会遇到过在 Fragment 用路由跳转到带成果的页面后不回调 onActivityResult() 的问题。原因是 ARouter 路由跳转时调用的是 Activity 的 startActivityForResult(),网上的解决计划是想方法改成调用 Fragment 的 startActivityForResult()。

不过现在 startActivityForResult() 已经弃用了,官方推荐用 Activity Result API 来代替。而 ActivityResult API 和路由的用法都比较特别,测验过会发现欠好适配,而且许多路由结构也没适配。个人探索了一下如何给路由结构适配 ActivityResult API,发现仍是有些方法适的。仅仅根据路由常用的 API 欠好完成,或许要了解下源码找下完成思路。

下面就给咱们共享一些路由结构适配 ActivityResult API 的计划。

ActivityResult 基础用法

先了解一下 ActivityResult API 怎样运用,首先要添加依靠:

dependencies {
    implementation "androidx.activity:activity-ktx:1.2.4"
}

在ComponentActivity或Fragment中调用 ActivityResult API 供给的registerForActivityResult()函数注册成果回调 (要在 onStart() 之前),该函数回来一个能够跳转页面的ActivityResultLauncher方针。

private val launcher = registerForActivityResult(StartActivityForResult()) {
  if (it.resultCode == RESULT_OK) {
    val intent = it.data
    // 处理回调成果
  }
}

调用 ActivityResultLauncher 的 launch() 函数就能跳转页面。

launcher.launch(Intent(this, SignInActivity::class.java))

这就能代替 startActivityForResult()和 onActivityResult() 了,可是官方并不是期望咱们这么用。registerForActivityResult() 函数有个 ActivityResultContract 类型的参数,顾名思义这是个协议,能决议输入参数的类型和回调成果的类型。

前面的 StartActivityForResult 协议类是在 launch() 时是输入了一个 intent,后边输出 resultCode 和 data,这是一个通用的协议。官方还完成了许多协议类,比如 GetContent 获取手机的内容,输入图片、视频、音频等 mime 类型,会跳转系统的内容选择器,选择内容后回调一个 uri。

private val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
  // Handle the returned Uri
}
override fun onCreate(savedInstanceState: Bundle?) {
  // ...
  selectButton.setOnClickListener {
    // Pass in the mime type you'd like to allow the user to select
    // as the input
    getContent.launch("image/*")
  }
}

官方还供给了申请权限、拍照、录视频等常用的协议类,咱们也能自定义所需的协议类。比如常见的跳转文字输入页面,咱们能够在启动时输入一个 name 去更改标题,回调函数输出编辑框的内容。下面是协议的完成,要承继 ActivityResultContract:

class InputTextResultContract : ActivityResultContract<String, String>() {
  override fun createIntent(context: Context, input: String?) =
    Intent(context, InputTextActivity::class.java)
      .putExtra(KEY_NAME, input)
  override fun parseResult(resultCode: Int, intent: Intent?): String? =
    if (resultCode == Activity.RESULT_OK) intent?.getStringExtra(KEY_VALUE) else null
}

ActivityResultContract 类有两个泛型需求声明,表明 launch() 参数的类型和回调成果的类型。还有两个函数需求重写,一个创立 intent,一个解析成果,都很简单了解就不多赘述了。

那么咱们也能够写一个路由的 ActivityResultContract,输入一个 path 就能跳转页面。

自定义路由的 ActivityResultContract

这儿有个难点是创立 intent 的时分用到 Activity 的 Class 方针,咱们要怎样获取呢?查下路由结构的文档并不能找到相关的方法,那就只能从路由结构的源码找到答案了。

而路由结构的源码这么多,该怎样去阅览找到答案呢?只要记住一点,找要害头绪

这儿的要害头绪是什么呢?咱们想一下,路由结构能用一个 path 跳转到对应的 Activity,还能怎样跳转呢?页面跳转是欠好脱离平台特性去别的完成的,就像 Retrofit 在 Android 切线程仍是得用 Handler,所以路由终究要么显现 intent 跳转要么隐式 intent 跳转。隐式跳转要改 manifests 不太或许,那就大约率是用了显现 intent 跳转。

所以咱们就有了要害头绪:startActivity(intent) 代码。要从哪里开端找呢?履行了哪个函数能跳转页面就从哪个函数开端找,ARouter 是调用了 navigation() 函数后才跳转页面,咱们就从该函数的源码开端找,也能够 debug 走一遍。

终究咱们能在 _ARouter 类的 startActivity() 函数里找到跳转的代码。ActivityCompat 是在曾经 v4 包引入的,内部仍是调用了 startActivity(intent)。

如何更好地进行 Android 组件化开发(三)ActivityResult 篇

第一个要害头绪找到后,咱们就能得到第二个要害头绪:intent 方针。经过 intent 方针咱们就能知道要跳转什么 Class,要么是 Intent 的结构函数设置,要么是 intent.setClass() 函数设置,咱们往上找一找调用的代码。

如何更好地进行 Android 组件化开发(三)ActivityResult 篇

看到这儿的源码咱们也能大约了解到路由的流程,经过 path 能知道对应的 Class 是什么类型,然后根据不同的类型去做不同的事。是 Activity 类型,就调用 startActivity(intent) 跳转页面。能够看到是 Intent 的结构函数设置了方针 Activity 的 Class,传入的是 postcard.getDestination()。

既然 Postcard#getDestination() 回来一个 Class 方针,那么第三个要害头绪便是 Postcard#setDestination(destination) 函数,咱们要找到 Class 的来源。点击去发现只有一处地方用到。

如何更好地进行 Android 组件化开发(三)ActivityResult 篇

能够看到传入的是 routeMeta.getDestination(),同理得到第四个头绪:RouteMeta#setDestination(destination) 函数。点击后会发现跳回上面同一行代码,由于 Postcard 是承继了 RouteMeta,仅有一处 postcard.setDestination(…) 调用了该函数,那必定不是这儿设置的。

这样形似头绪断了?其实不然,特点不必定的是 set() 函数设置的,还有或许是结构函数传进来的。所以第四个要害头绪应该改成 routeMeta 方针,看下是怎样得到的。

如何更好地进行 Android 组件化开发(三)ActivityResult 篇

能够看到是 Warehouse.routes 查出来的,所以这便是下一个头绪,跳进去看下是什么。

如何更好地进行 Android 组件化开发(三)ActivityResult 篇

到这儿咱们就能看到 routes 是一个 HashMap,key 的类型是 String,能够经过 path 去查出路由信息 routeMeta,之后能获取到对应类的 Class 方针。这其实便是一个路由表,保存着路由信息。

咱们终于找到了一个经过路由表获取 Class 方针的方法:

val clazz = Warehouse.routes[path]?.destination

能够自定义一个路由的协议类了,首先要考虑输入参数类型和输出参数类型,咱们就用官方自带的 ActivityResult 作为输出参数类型,能够获取 resultCode 和 data。可是要用什么作为输入参数类型呢?一个 path 必定不可,咱们或许要传参或设置 flag 什么的,所以咱们增加一个 RouteRequest 类作为输入参数的类型,包括 path 和 intent,intent 能够弥补 Class 以外的信息。

data class RouteRequest(
  val path: String,
  val intent: Intent = Intent()
)

创立一个类承继 ActivityResultContract,其间创立 Intent 的函数就用 RouteRequest 的 intent 方针去弥补 Class 信息,判别一下不是 Activity 类型就抛异常,由于不是 Activity 的话也无法跳转。别的 Warehouse 的 routes 方针没有用 public 修饰,所以要访问该方针的话还需求反射一下。

class StartRouteActivityContract : ActivityResultContract<RouteRequest, ActivityResult>() {
  override fun createIntent(context: Context, input: RouteRequest) =
    input.intent.apply {
      val routeMeta = routes[input.path]
      if (routeMeta?.type != RouteType.ACTIVITY) {
        throw IllegalArgumentException("The routing class for the path is not an activity type.")
      }
      setClass(context, routeMeta.destination)
    }
  override fun parseResult(resultCode: Int, intent: Intent?) = ActivityResult(resultCode, intent)
  companion object{
    @Suppress("UNCHECKED_CAST")
    private val routes: Map<String, RouteMeta> by lazy {
      val clazz = Class.forName("com.alibaba.android.arouter.core.Warehouse")
      val field = clazz.getDeclaredField("routes")
      field.isAccessible = true
      field[null] as Map<String, RouteMeta>
    }
  }
}

咱们来调用看看,下面便是标准的 ActivityResult API 用法了。

private val launcher = registerForActivityResult(StartRouteActivityContract()) {
  if (it.resultCode == RESULT_OK) {
    // 处理回调成果
  }
}
launcher.launch(RouteRequest("/xxx/xxx"))

以上的思路适用于绝大多数路由结构去适配 ActivityResult API,要害是要知道怎样才能用 path 去获取对应的 Class 方针。路由结构根本都会用一个路由表缓存着路由的映射联系,那就能以路由表为方针。又由于跳转页面离不开平台特性,路由工具跳转页面的函数终究仍是会履行 startActivity(intent),以此为头绪就大约率能一步步地找到路由表的方位。

计划改进

不必反射

一般路由结构是不会把路由表给暴露的,就比如 ARouter 的路由表 Warehouse.routes 没有对外开放。那么用前面阅览源码的思路找到路由表的方位后,往往仍是要用到反射得到路由表方针的。尽管一次反射的性能能够忽略不计,可是有人看到反射就很非常反感。

那么有没什么方法不必反射就能查出所需的 Class 方针呢?或许会有的,可是估量要对整个路由结构的完成流程比较了解后,才更简单找到不必反射的完成方法。下面以 ARouter 为例抛砖引玉一下。

咱们先用个寄信流程类比 ARouter 的路由流程,其间有两个类咱们需求特别关注,LogisticsCenter 和 Postcard,代表物流中心和明信片。

一开端初始化,物流中心会让各个省份到库房的一个表格填写每个具体地址 (xx 省 xx 市 xxxxxxx) 对应的目的地方位(经纬度),这样准备工作就完成了。然后便是寄信,咱们从邮递员手中拿一张明信片填写地址:xx 省 xx 市 xxxxxxx,填好后交给邮递员。邮递员看到一个具体到门牌号的地址并不必定清楚确切的方位,就先拿明信片给物流中心去弥补该地址对应的具体方位,知道具体方位后就能把信送出去了。

了解这个流程后,咱们再来看路由的流程,首先是初始化:

ARouter.init(this)

ARouter 初始化的时分会履行 LogisticsCenter.init(context) 初始化物流中心,物流中心会让各个模块到库房的路由表 Warehouse.routes 设置每个 path 对应哪些 Class 信息。

之后就能调用路由的导航方法了,咱们一般是直接链式调用,其实会阅历了两个进程。首先是 build(path) 函数回来了一个 Postcard 明信片方针,而之后的 postcard.navigation() 函数在内部会调用 ARouter.getInstance().navigation(postcard)。所以咱们链式调用的代码等效于:

val postcard = ARouter.getInstance().build("/app/main")
ARouter.getInstance().navigation(postcard)

咱们把 ARouter 当作邮递员,这就能对应前面说的,在邮递员手上拿一张明信片填地址,再交回给邮递员去邮寄。而快递员 ARouter 在 navigation(postcard) 的时分,并不知道 postcard.path 对应的 Class 是什么,就会调用:

LogisticsCenter.completion(postcard)

让物流中心到库房的路由表用 path 查出 Class,设置到 postcard.destination 中。这样 ARouter 就能用 postcard.destination 得到 Class 方针,再根据类型去跳转 Activity 或许实例化方针。

以上便是 ARouter 主要的路由流程了,咱们能知道 ARouter 会拿 postcard 给到 LogisticsCenter 弥补对应的 Class 信息。那么能够不必劳烦 ARouter,咱们自己亲身把 postcard 交给 LogisticsCenter,得到路由的 Class 信息后想做什么都能够。

val postcard = ARouter.getInstance().build(input.path)
LogisticsCenter.completion(postcard)
val clazz = postcard.destination

思路有了,优化一下前面的自定义 ActivityResultContract,把反射路由表的代码去掉,改成用 LogisticsCenter 弥补 Class 信息。

class StartRouteActivityContract : ActivityResultContract<RouteRequest, ActivityResult>() {
  override fun createIntent(context: Context, input: RouteRequest) =
    input.intent.apply {
      val postcard = ARouter.getInstance().build(input.path)
      LogisticsCenter.completion(postcard)
      if (postcard?.type != RouteType.ACTIVITY) {
        throw IllegalArgumentException("The routing class for the path is not an activity type.")
      }
      setClass(context, postcard.destination)
    }
  override fun parseResult(resultCode: Int, intent: Intent?) = ActivityResult(resultCode, intent)
}

这样就能不必反射完成 ActivityResult API 了,不过这个思路不必定适合其它路由结构,或许没有类似 LogisticsCenter 的角色或许相关函数只能在内部访问。假如还得用到反射,就不如用前面的思路反射路由表方针了。

完善路由阻拦功用

大多数需求仅仅代替 startActivityForResult(intent),上述的自定义 ActivityResultContract 封装就能满足,可是假如还有路由阻拦需求的话就不适用了。

怎样保存这部分逻辑个人考虑了很久,最开端是测验重写 ActivityResultContract 的 getSynchronousResult() 函数,假如回来值不为 null,则会中断跳转的操作。

比如封装敞开蓝牙的功用,假如蓝牙已经敞开了,就无需调用 startActivityForResult(intent) 敞开蓝牙了。所以在 getSynchronousResult() 函数验证了蓝牙已敞开就回来 SynchronousResult(true),这会直接走蓝牙敞开成功的回调。

class EnableBluetoothContract : ActivityResultContract<Unit, Boolean>() {
  override fun createIntent(context: Context, input: Unit?) =
    Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
  override fun parseResult(resultCode: Int, intent: Intent?) =
    resultCode == Activity.RESULT_OK
  override fun getSynchronousResult(context: Context, input: Unit?): SynchronousResult<Boolean>? =
    if (isBluetoothEnabled) SynchronousResult(true) else null
}

那么同理判别到需求路由阻拦的时分回来一个撤销跳转的成果不就行了?略微测验后发现不可,由于路由阻拦是异步操作,而 getSynchronousResult() 函数是需求回来一个同步成果。

这条路走不通后又考虑了很久,想到了另一个思路。路由结构一般会传一个 requestCode 参数,假如是一个正数就会调用 startActivityForResult(intent)。那么能够改成支持传一个 launcher 方针,当要跳转页面的时分就履行 launcher.launch(intent),其它的逻辑都不变。终究改成下面的用法:

private val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
  if (it.resultCode == RESULT_OK) {
    // ...
  }
}
ARouter.getInstance().build("/account/sign_in").navigation(this, launcher)

先找到传入了 requestCode 参数而且具有路由逻辑的函数,咱们读下源码发现是集中到了 _ARouter 类的一个 navigation() 函数进行处理:

如何更好地进行 Android 组件化开发(三)ActivityResult 篇

把该函数触及的一切源码悉数拷贝出来,将 requestCode 参数改成 launcher 参数,再将 startActivityForResult(intent) 函数改为 launcher.launch(intent),其它的逻辑代码悉数保存。而且把函数改成 Postcard 的扩展函数,移除 postcard 参数。

这儿有个难点是保存一切逻辑,由于或许会调用到非 public 的特点或函数,这就要找代替计划了。而 ARouter 需求修正的并不多,找到以下的代替方法:

  • logger 改为 ARouter.logger
  • debuggable() 改成 ARouter.debuggable()

咱们就能完成以下扩展,代码会有点多,但逻辑更完整。

fun Postcard.navigation(context: Context, launcher: ActivityResultLauncher<Intent>, callback: NavigationCallback? = null): Any? {
  val pretreatmentService = ARouter.getInstance().navigation(PretreatmentService::class.java)
  if (null != pretreatmentService && !pretreatmentService.onPretreatment(context, this)) {
    // Pretreatment failed, navigation canceled.
    return null
  }
  // Set context to postcard.
  this.context = context
  try {
    LogisticsCenter.completion(this)
  } catch (ex: NoRouteFoundException) {
    ARouter.logger.warning(Consts.TAG, ex.message)
    if (ARouter.debuggable()) {
      runInMainThread {
        val text = "There's no route matched!\nPath = [$path]\nGroup = [$group]"
        Toast.makeText(context, text, Toast.LENGTH_LONG).show()
      }
    }
    if (null != callback) {
      callback.onLost(this)
    } else {
      // No callback for this invoke, then we use the global degrade service.
      ARouter.getInstance().navigation(DegradeService::class.java)
        ?.onLost(context, this)
    }
    return null
  }
  callback?.onFound(this)
  if (!isGreenChannel) { // It must be run in async thread, maybe interceptor cost too mush time made ANR.
    interceptorService.doInterceptions(this, object : InterceptorCallback {
      override fun onContinue(postcard: Postcard) {
        postcard._navigation(launcher, callback)
      }
      override fun onInterrupt(exception: Throwable) {
        callback?.onInterrupt(this@navigation)
        ARouter.logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.message)
      }
    })
  } else {
    return _navigation(launcher, callback)
  }
  return null
}
@Suppress("FunctionName", "DEPRECATION")
private fun Postcard._navigation(launcher: ActivityResultLauncher<Intent>, callback: NavigationCallback?): Any? {
  val currentContext = context
  when (type) {
    RouteType.ACTIVITY -> {
      // Build intent
      val intent = Intent(currentContext, destination).putExtras(extras)
      // Set flags.
      val flags = flags
      if (0 != flags) {
        intent.flags = flags
      }
      // Non activity, need FLAG_ACTIVITY_NEW_TASK
      val action = action
      if (!TextUtils.isEmpty(action)) {
        intent.action = action
      }
      // Navigation in main looper.
      runInMainThread {
        launcher.launch(intent)
        if (-1 != enterAnim && -1 != exitAnim && currentContext is Activity) {    // Old version.
          currentContext.overridePendingTransition(enterAnim, exitAnim)
        }
        callback?.onArrival(this)
      }
    }
    RouteType.PROVIDER -> return provider
    RouteType.BOARDCAST, RouteType.CONTENT_PROVIDER, RouteType.FRAGMENT -> {
      val fragmentMeta = destination
      try {
        val instance = fragmentMeta.getConstructor().newInstance()
        if (instance is android.app.Fragment) {
          instance.arguments = extras
        } else if (instance is Fragment) {
          instance.arguments = extras
        }
        return instance
      } catch (ex: java.lang.Exception) {
        ARouter.logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.stackTrace))
      }
      return null
    }
    RouteType.METHOD, RouteType.SERVICE -> return null
    else -> return null
  }
  return null
}
private val interceptorService by lazy {
  ARouter.getInstance().build("/arouter/service/interceptor").navigation() as InterceptorService
}
private val handler by lazy { Handler(Looper.getMainLooper()) }
private fun runInMainThread(runnable: Runnable) {
  if (Looper.getMainLooper().thread !== Thread.currentThread()) {
    handler.post(runnable)
  } else {
    runnable.run()
  }
}

这样就能把传 requestCode 改成传 launcher 方针,假如 path 对应是 Activity 类型,就会用 ActivityResult API 跳转页面,路由阻拦功用也会保存。

ARouter.getInstance().build("/account/sign_in").navigation(this, launcher)

还能够再优化一下用法,增加一个 ActivityResultLauncher 的扩展去调用该路由方法。

fun ActivityResultLauncher<Intent>.launchByRoute(
  context: Context, path: String, callback: NavigationCallback? = null, block: (Postcard.() -> Unit)? = null
) = ARouter.getInstance().build(path).apply { block?.invoke(this) }.navigation(context, this, callback)

终究就更趋近于 ActivityResult API 本来的用法。

private val launcher = registerForActivityResult(StartActivityForResult()) {
  if (it.resultCode == RESULT_OK) {
    // ...
  }
}
launcher.launchByRoute(this, "/account/sign_in")

以上便是 ARouter 结合 ActivityResult API 比较完美的计划了,预处理和路由阻拦的逻辑都有保存。这个思路可用于其它路由结构,便是有个难点要保存一切的路由逻辑,假如有非 public 的特点或函数就要找代替计划,没代替计划就或许得用反射了。

总结

本文共享了 ActivityResult API 的基础用法,供给了两个适配路由结构的计划:

  • 自定义 ActivityResultContract,代码量很少,满足常见的跳转页面回来成果的需求。可是要阅览代码找到路由表的问题,想不必反射或许要对全体路由流程比较了解。
  • 用 launcher 参数代替 requestCode 参数,保存了路由阻拦功用,逻辑愈加完善。可是修正成本或许会比较高,而且代码量会许多。

关于我

一个兴趣使然的程序“工匠” 。有代码洁癖,喜欢封装,对封装有必定的个人见解,有不少个人原创的封装思路。GitHub 有共享一些帮助搭建开发结构的开源库,有任何运用上的问题或许需求都能够提 issues 或许加我微信直接反馈。

  • :/user/419539…
  • GitHub:github.com/DylanCaiCod…
  • 微信号:DylanCaiCoding