自定义 WMRouter 的 Activity 启动逻辑
本文最后更新于 805 天前,其中的信息可能已经有所发展或是发生改变。

0x00 写在前面

WMRouter 是美团出品的一款 Android 路由框架,它存在的一个问题就是启动 Activity 后的返回数据很难处理,还是要在 Activity 中去写 onActivityResult() 方法。比如下面这样:

class MainActivity : AppCompatActivity() {
  	override fun onCreate(savedInstanceState: Bundle?) {
      	// ...
      
      	val button = findViewById<Button>(R.id.main_to_second)
      	button.setOnClickListener(v -> DefaultUriRequest(this, "second")
                .activityRequestCode(100)
                .start())
    }
  
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == 100) {
            // ...
        }
    }
}

这样的好处是,如果你的项目原先不是使用 WMRouter 的话,那么接入 WMRouter 以后只需要修改启动部分的代码,而不需要修改接收部分的逻辑。但是现在 onActivityResult() 已经被官方标记为废弃了,所以当然要尽快迁移到新的 API 中。而 WMRouter 又不支持新的 Activity Result API,这就变得有些尴尬。

看着一行被划掉的线,终究还是觉得不爽
看着一行被划掉的线,终究还是觉得不爽

这篇文章介绍了一种新的封装方式,使用了 Activity Result API 进行封装,通过一个不可见的 Fragment 来启动 Activity 和接收来自 Activity 返回的数据,再通过 WMRouter 的接口来替换默认的启动 Activity 相关的代码。

0x01 封装 Activity Result API

首先需要封装 Activity Result API。封装方式可以参考 Android 权限请求封装,同样是创建一个 BaseActivityResultLauncher 和它的子类,用于提供一个方便可用的 ActivityResultLauncher

abstract class BaseActivityResultLauncher<I, O>(
    caller: ActivityResultCaller, contract: ActivityResultContract<I, O>) {
    private var callback: ActivityResultCallback<O>? = null
 
    // 注册 ActivityResultLauncher
    private var launcher: ActivityResultLauncher<I> = caller.registerForActivityResult(contract) {
        callback?.onActivityResult(it)
        callback = null
    }
 
    protected fun launchInternal(input: I, callback: ActivityResultCallback<O>) {
        this.callback = callback
        launcher.launch(input)
    }
}
class StartActivityLauncher(caller: ActivityResultCaller) : BaseActivityResultLauncher<Intent, ActivityResult>(
    caller, ActivityResultContracts.StartActivityForResult()
) {

    /**
     * 启动 Activity
     * 
     * @param intent 启动 Activity 的 Intent
     * @param callback 结果回调
     */
    fun launch(intent: Intent, callback: (Int, Intent?) -> Unit) {
        launchInternal(intent) { result ->
            callback(result.resultCode, result.data)
        }
    }
}

0x02 使用 Fragment 接收结果

接下来创建一个不可见的 Fragment,这个 Fragment 主要是用来启动 Activity 以及接收来自 Activity 返回的信息的。它实际上就是使用了 StartActivityLauncher 来实现的启动以及结果回调的。

class ForResultFragment : Fragment() {
    private val launcher = StartActivityLauncher(this)
    private lateinit var intent: Intent
    private lateinit var callback: (Int, Intent?) -> Unit

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        launcher.launch(intent) { code, data ->
            callback(code, data)
            removeFragment()
        }
    }

    private fun removeFragment() {
        try {
            parentFragmentManager.beginTransaction().remove(this).commitAllowingStateLoss()
        } catch (e: Exception) {
            LogUtils.e("ForResultFragment", "remove fragment failed.", e)
        }
    }

    fun setIntent(intent: Intent): ForResultFragment {
        this.intent = intent
        return this
    }

    fun setCallback(callback: (Int, Intent?) -> Unit): ForResultFragment {
        this.callback = callback
        return this
    }
}

接下来是把通过 ForResultFragment 来启动 Activity 以及接收结果的逻辑集成到 WMRouter 中,这样就可以保留使用路径来启动 Activity 的功能,同时又能在发起请求时自定义接收的逻辑。

0x03 将自定义逻辑集成到 WMRouter

如何集成到 WMRouter

现在已经准备好了启动 Activity 和接收 Activity 返回的结果的 API 了,接下来要做的就是把这套逻辑集成到 WMRouter 里面去。而这个集成也无非就是两个关键点:

  1. 替换掉默认的启动 Activity 逻辑,换成由 ForResultFragment 来启动;
  2. UriRequest 加上接收结果的回调。

有关 WMRouter 是如何实现启动 Activity 的,请参考 WMRouter 启动 Activity 源码解析。从该篇分析中可以得到两个简单的结论:

  • 启动 Activity 的地方是 DefaultActivityLauncher 类,实现了默认的 Activity 启动逻辑;
  • 通过 RouterComponents.setActivityLauncher() 方法可以替换 ActivityLauncher,实现自定义的启动逻辑。

要把 ForResultFragment 启动 Activity 的逻辑集成到 WMRouter 中,就需要通过 RouterComponents.setActivityLauncher() 方法来设置自定义的 ActivityLauncher。所以我们可以继承 DefaultActivityLauncher 类并重写相应的方法,实现自定义启动逻辑。

注意这个 ActivityLauncher 是 WMRouter 中的一个接口,不是 Activity Result API 中的类。

而至于给 UriRequest 加上回调,我则是通过扩展函数来扩展 DefaultUriRequest。当然,也可以选择继承 DefaultUriRequest,效果都是差不多的。

扩展 DefaultUriRequest

创建一个 ForResultUriRequest.kt,代码如下:

// ForResultUriRequest.kt

private const val ON_RESULT_CALLBACK = "StartActivityLauncher.OnResultCallback"

private interface OnResultCallback {
    fun onResult(code: Int, data: Intent?)
}

fun DefaultUriRequest.hasOnResultCallback(): Boolean {
    val callback = getField(OnResultCallback::class.java, ON_RESULT_CALLBACK)
    return callback != null
}

fun DefaultUriRequest.getOnResultCallback(): ((Int, Intent?) -> Unit)? {
    val callback = getField(OnResultCallback::class.java, ON_RESULT_CALLBACK) ?: return null
    return { code, data ->
        callback.onResult(code, data)
    }
}

fun DefaultUriRequest.onResult(callback: (Int, Intent?) -> Unit): DefaultUriRequest {
    putField(ON_RESULT_CALLBACK, object : OnResultCallback {
        override fun onResult(code: Int, data: Intent?) {
            callback(code, data)
        }
    })
    return this
}

实现自定义的 ActivityLauncher

重写的 ActivityLauncher 实现类如下:

object ForResultActivityLauncher : DefaultActivityLauncher() {
    override fun startActivityByAction(
        request: UriRequest,
        intent: Intent,
        internal: Boolean
    ): Int {
        return UriResult.CODE_NOT_FOUND
    }

    override fun startActivityByDefault(
        request: UriRequest?,
        context: Context,
        intent: Intent,
        requestCode: Int?,
        internal: Boolean
    ): Int {
        return try {
            if (request == null) {
                return UriResult.CODE_BAD_REQUEST
            }
            val options = request.getField(Bundle::class.java, ActivityLauncher.FIELD_START_ACTIVITY_OPTIONS)
            if (context is FragmentActivity && request is DefaultUriRequest && request.hasOnResultCallback()) {
                startForResult(context, intent) { code, data ->
                    request.getOnResultCallback()?.invoke(code, data)
                }
            } else {
                ActivityCompat.startActivity(context, intent, options)
            }
            doAnimation(request)
            request.putField(ActivityLauncher.FIELD_STARTED_ACTIVITY, if (internal) {
                ActivityLauncher.INTERNAL_ACTIVITY
            } else {
                ActivityLauncher.EXTERNAL_ACTIVITY
            })
            UriResult.CODE_SUCCESS
        } catch (e: ActivityNotFoundException) {
            Debugger.e(e)
            UriResult.CODE_NOT_FOUND
        } catch (e: SecurityException) {
            Debugger.e(e)
            UriResult.CODE_FORBIDDEN
        } catch (e: Exception) {
            Debugger.e(e)
            UriResult.CODE_ERROR
        }
    }

    private fun startForResult(
        activity: FragmentActivity,
        intent: Intent,
        callback: (Int, Intent?) -> Unit
    ) {
        val fragment = ForResultFragment()
            .setIntent(intent)
            .setCallback(callback)
        val transaction = activity.supportFragmentManager
            .beginTransaction()
        if (fragment.isAdded) {
            transaction.remove(fragment)
        }
        transaction.add(fragment, fragment.toString())
            .commitAllowingStateLoss()
    }
}

在 Application 中使用自定义的 ActivityLauncher

最后在 Application 中设置一下即可。

class App : Application() {
    override fun onCreate() {
        // ...

        // 用 RouterComponents 设置自定义的 ActivityLauncher
        RouterComponents.setActivityLauncher(ForResultActivityLauncher)
        Router.init(DefaultRootUriHandler(this))
    }
}

0x04 使用效果

现在就可以试试看效果了。首先看一个 MainActivity 如何启动 SecondActivity 并处理返回的结果:

class MainActivity : BaseActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.main_go).setOnClickListener {
            DefaultUriRequest(this, "/second")
                .onResult { i, _ ->
                    Log.d("MainActivity", "receive data!!! code: $i")
                }.start()
        }
    }
}

然后是 SecondActivity 如何返回结果:

@RouterUri(path = ["/second"])
class SecondActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)

        findViewById<Button>(R.id.second_back).setOnClickListener {
            setResult(RESULT_OK)
            finish()
        }
    }
}

0xFF 写在最后

这篇文章算是补上了之前留的坑,也就是分析了 WMRouter 的启动 Activity 源码以后,根据分析的结果来自定义一下启动 Activity 的逻辑。

下一篇应该会分享一下 WMRouter 的 ServiceLoader 部分的源码。看看啥时候有空吧。

暂无评论

发送评论 编辑评论

上一篇
下一篇