startActivityForResult与ARouter在fragment中使用startActivityForResult

4,246 阅读4分钟

来自第二天的更新

关于文中使用空Fragment进行回调的方案,在activity被销毁重建后会有问题,具体是从这篇文章是时候丢掉 onActivityResult 了 !的评论一木艮呆毛的回复中了解到的,并且写demo试了一下的确存在这个情况。

例如以下的这种写法,activity的重建让原先方法引用中的btn不是现在重建后activity中的btn。

NoResult.startActivityForResult(activity, intent, requestCode){ _: Int, _: Int, _: Intent? ->
	btn?.text = "2002"
}

关于startActivityForResult

startActivityForResult在平时的开发中用到的并不多,但是用到也是非常麻烦。需要重写onActivityResult,需要在老远的地方写回调处理,当然现在也有很多开源库处理了这些问题。 主要以下几个原因,决定还是想自己写一些工具来处理这个问题。

  1. 解决方案不复杂,自己能够完成基础的功能。在使用频度较低的情况下用kotlin的基础语法实现即可,不一定需要链式和Rx扩展。
  2. startActivityForResult用到的机会并不是特别多,在项目中因为很小的需求额外引入一个三方库是没必要的
  3. 自己写主动权更大

解决方案

解决方案和RxPermissions的类似,通过一个隐藏的fragment做转发,所有的startActivityForResult都由这个fragment发起,直接使用SparseArray存储方法引用可以少写接口(kotlin的便利)。retainInstance = true可以让fragment不会被销毁脱离Activity存在。fragment的方法startActivityForResult最终会回调它所在的activity的startActivityFromFragment保证结果会最终回调到该fragment。

class ResultFragment : Fragment() {

    private val callbacks =
        SparseArray<(requestCode: Int, resultCode: Int, data: Intent?) -> Unit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        retainInstance = true
    }

    fun startActivityForResult(
        intent: Intent,
        requestCode: Int,
        callback: (requestCode: Int, resultCode: Int, data: Intent?) -> Unit
    ) {
        callbacks.put(requestCode, callback)
        startActivityForResult(intent, requestCode)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        callbacks.get(requestCode)?.invoke(requestCode, resultCode, data)
        callbacks.remove(requestCode)
    }
}

然后通过方法查找该fragment

//通过tag找到fragment
private fun findOnResultFragment(activity: FragmentActivity): ResultFragment? =
        activity.supportFragmentManager.findFragmentByTag(tag) as? ResultFragment
//在找不到fragment的时候创建新的fragment
private fun getOnResultFragment(activity: FragmentActivity): ResultFragment {
    return findOnResultFragment(activity) ?: activity.supportFragmentManager.run {
        ResultFragment().apply {
            beginTransaction().add(this, NoResult.tag).commitAllowingStateLoss()
            executePendingTransactions()
        }
    }
}

最后提供startActivityForResult方法

fun startActivityForResult(
        fragment: Fragment, intent: Intent, requestCode: Int,
        callback: (requestCode: Int, resultCode: Int, data: Intent?) -> Unit) {
    startActivityForResult(fragment.requireActivity(), intent, requestCode, callback)
}

fun startActivityForResult(
        activity: FragmentActivity, intent: Intent, requestCode: Int,
        callback: (requestCode: Int, resultCode: Int, data: Intent?) -> Unit) {
    getOnResultFragment(activity).startActivityForResult(intent, requestCode, callback)
}

实际使用的时候可以直接这样使用,调整上面的方法处理掉requestCode和resultCode可以让方法更简洁。

NoResult.startActivityForResult(fragment/activity, intent, requestCode){ requestCode: Int, resultCode: Int, intent: Intent? ->
	//处理结果...
}

ARouter中fragment调用startActivityForResult的问题

参考问题#489 关于在fragment 使用 navigation(Activity mContext, int requestCode) 中的问题,使用ARouter在fragment中调用startActivityForResult相关逻辑时不回调fragment的onActivityResult方法。

这个问题是因为ARouter实现原理造成的,在fragment中使用ARouter的startActivityForResult会使用fragment所依附的activity的startActivityForResult方法,而实际fragment中startActivityForResult最终实现是调用它所依附的activity的startActivityFromFragment,所以这就造成了上述问题。

ARouter大致原理是通过注解和APT工具生成映射代码,然后在APP安装后首次打开或更新后首次打开时扫描文件通过反射加载映射缓存至SharedPreferences。在使用跳转时构建Postcard对象然后进行路由跳转,Postcard对象中包含了本次路由跳转的所有信息。

既然Postcard中包含了路由的所有信息,那就自己处理startActivityForResult时相关的逻辑。文章由集成ARouter引发的一些思考中通过修改源码做了相关的实现,但因为ARouter中Postcardpublic的,所以只要为它添加扩展方法来实现即可。

//预处理方法,原ARouter中的实现,不过舍弃了拦截器和自定义callback
internal fun Postcard.pretreatment(context: Context? = null): Postcard? {
    val pretreatmentService = ARouter.getInstance().navigation(PretreatmentService::class.java)
    if (null != pretreatmentService && !pretreatmentService.onPretreatment(context, this)) {
        // Pretreatment failed, navigation canceled.
        return null
    }
    try {
        LogisticsCenter.completion(this)
    } catch (e: Exception) {
        //这里弹toast 未找到router
        //回调call
        ARouter.getInstance().navigation(DegradeService::class.java)
            ?.apply { onLost(context, this@pretreatment) }
        return null
    }
    //不考虑实用拦截器
    //只考虑activity
    if (type != RouteType.ACTIVITY) return null
    return this
}

使用Postcard来构建Intent,不需要太复杂

internal fun Postcard.buildIntent(activity: Activity): Intent {
    return Intent(activity, destination).apply {
        putExtras(this@buildIntent.extras)
        if (this@buildIntent.flags != -1)
            flags = this@buildIntent.flags
        if (!this@buildIntent.action.isNullOrEmpty())
            action = this@buildIntent.action
    }
}

最后提供startActivityForResult的方法,包含了最上面的实现

fun Postcard.navigateForResult(
    activity: FragmentActivity, requestCode: Int,
    callback: (requestCode: Int, resultCode: Int, data: Intent?) -> Unit
) {
    pretreatment(activity)?.apply {
        NoResult.startActivityForResult(activity, buildIntent(activity), requestCode, callback)
    }
}

fun Postcard.navigateForResult(
    fragment: Fragment, requestCode: Int,
    callback: (requestCode: Int, resultCode: Int, data: Intent?) -> Unit
) {
    pretreatment(fragment.requireContext())?.apply {
        NoResult.startActivityForResult(fragment, buildIntent(fragment.requireActivity()), requestCode, callback)
    }
}

这里其实还提供了一个普通的startActivity方法。ARouter中直接导航的方法是使用Application的context进行跳转的,这时候就不得不要为跳转intent添加FLAG_ACTIVITY_NEW_TASK。这里涉及taskAffinity属性用于Activity栈分配,如果有特殊需求的话,ARouter原有的跳转方式可能不太合适,所以可以添加以下这种跳转方式来避免添加FLAG_ACTIVITY_NEW_TASK。这里标记一篇文章以便日后复习:Activity的启动模式.

fun Postcard.navigateActivity(activity: FragmentActivity) {
    pretreatment(activity)?.apply { activity.startActivity(buildIntent(activity)) }
}

结语

以上简单的实现基本能够满足日常使用,如果更复杂的使用场景那么可以选择更加完善易用的三方库。还有,不同的启动模式会对startActivityForResult造成各种影响,切记切记。