业务代码参数透传满天飞?(一)

9,176 阅读11分钟

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

引子

项目中参数多级透传满天飞的情况很常见,增加了开发的复杂度、出错的可能、及维护的的难度。

透传包括两种形式:

  1. 不同界面之间参数透传。
  2. 同一界面中不同层级控件间透传。

该系列的目标是消除这两种参数透传,使得不同界面以及同一界面内各层级间更加解耦,降低参数传递开发的复杂度,减少出错的可能,增加可维护性。

本篇先聚焦在第一个 case,即不同界面间参数透传:

// xxxActivity.java
private void parseIntent() {
   Bundle paramsCtrl = getIntent().getBundleExtra(RouterConstant.ROUTER_PARAM_CONTROL);
   if (paramsCtrl != null) {
       mMaxFootageNumber = paramsCtrl.getInt(MaterialConfig.ARG_MATERIAL_MAX_FOOTAGE_NUMBER, -1);
       minRangeDuration = paramsCtrl.getLong(MaterialConfig.ARG_KEY_MATERIAL_MIN_DURATION, 0);
       maxRangeDuration = paramsCtrl.getLong(MaterialConfig.ARG_KEY_MATERIAL_MAX_DURATION, Long.MAX_VALUE);
       shearClipCapacityOn = paramsCtrl.getBoolean(MaterialConfig.ARG_KEY_SHEAR_CAPACITY_ON, true);
       mRouterFrom = paramsCtrl.getInt(MaterialProtocol.MATERIAL_SOURCE_KEY, MaterialProtocol.SOURCE.UNKNOWN);
       mTemplateFrom = paramsCtrl.getInt(VideoTemplateConfig.KEY_TEMPLATE_SOURCE, VideoTemplateConfig.SOURCE.UNKNOWN);
       mCategoryId = paramsCtrl.getString(VideoTemplateConfig.ARG_MATERIAL_TEMPLATE_TAB_ID, "");
       mCategoryName = paramsCtrl.getString(VideoTemplateConfig.ARG_MATERIAL_TEMPLATE_TAB_NAME, "");
       mTemplateTrace = paramsCtrl.getInt(VideoTemplateConfig.KEY_TEMPLATE_TRACE, VideoTemplateConfig.SOURCE.UNKNOWN);
       mTemplatesTraceV2 = paramsCtrl.getInt(VideoTemplateConfig.KEY_TEMPLATES_TRACE_V2, VideoTemplateConfig.TemplatesTraceV2.OTHER);
       mMaterialFrom = paramsCtrl.getInt(CommonConstant.EFFECTCENTER.TYPE, Integer.MIN_VALUE);
       mMaterialItem = (MediaItem) paramsCtrl.getSerializable(MaterialConfig.EXTRA_KEY_MATERIAL_FILE_PATH);
       mDefaultTabIndex = paramsCtrl.getInt(MaterialConfig.EXTRA_KEY_MATERIAL_INDEX, 0);
       mDefaultSubTabIndex = paramsCtrl.getInt(MaterialConfig.EXTRA_KEY_MATERIAL_SUB_INDEX, 0);
       mShowFolderTab = paramsCtrl.getBoolean(MaterialConfig.ARG_MATERIAL_SHOW_FOLDER_TAB, true);
       mShowBottomArea = paramsCtrl.getBoolean(MaterialConfig.ARG_MATERIAL_SHOW_BOTTOM_AREA, true);
       mMultiSelectMode = paramsCtrl.getBoolean(MaterialConfig.ARG_MATERIAL_SELECT_MULTI_MODE, true);
       mMaterialRemoteMode = paramsCtrl.getInt(MaterialConfig.ARG_MATERIAL_REMOTE_MODE, MaterialRemoteMode.HORIZONTAL);
       mClipDuration = paramsCtrl.getLong(MaterialConfig.ARG_MATERIAL_CLIP_DURATION, 0L);
       mShowCurrentProjectTab = paramsCtrl.getBoolean(MaterialConfig.ARG_MATERIAL_SHOW_CURRENT_PROJECT_TAB, true);
       mFootageConstraintList = (List<MediaItem>) paramsCtrl.getSerializable(MaterialConfig.ARG_MATERIAL_FOOTAGE_CONSTRAINT_LIST);
       mFootageDuration = paramsCtrl.getLong(MaterialConfig.ARG_MATERIAL_FOOTAGE_DURATION, 0L);
       mMinFootageNumber = paramsCtrl.getInt(MaterialConfig.ARG_MATERIAL_MIN_FOOTAGE_NUMBER, -1);
       mVideoTemplateMusicInfo = (VideoTemplateMusicBean) paramsCtrl.getSerializable(VideoTemplateConfig.ARG_MATERIAL_MUSIC_INFO);
       mVideoTemplateMusicList = (VideoTemplateMusicBean[]) paramsCtrl.getSerializable(VideoTemplateConfig.ARG_MATERIAL_MUSIC_LIST);
       mVideoTemplateId = paramsCtrl.getLong(VideoTemplateConfig.ARG_VIDEO_TEMPLATE_ID);
       hasPlayStyleId = paramsCtrl.getBoolean("hasPlayStyleId");
       catId = paramsCtrl.getLong(MaterialConfig.EXTRA_TUWEN_CAT_ID);
       catTemplateId = paramsCtrl.getLong(MaterialConfig.EXTRA_TUWEN_TEMPLATE_ID);
       mVideoTemplatePath = paramsCtrl.getString(VideoTemplateConfig.ARG_VIDEO_TEMPLATE_PATH);
       mVideoTemplateDraftId = paramsCtrl.getString(VideoTemplateConfig.ARG_VIDEO_TEMPLATE_DRAFT_ID, "");
       mVideoTemplateDownloadUrl = paramsCtrl.getString(VideoTemplateConfig.ARG_VIDEO_TEMPLATE_DOWNLOAD_URL);
       mVideoTemplateUpFrom = paramsCtrl.getInt(VideoTemplateConfig.ARG_TEMPLATE_UP_FROM, -1);
       mTemplatesUpFromV2 = VideoTemplateConfig.mapTemplatesUpFrom(mVideoTemplateUpFrom);
       mExpGrp = paramsCtrl.getString(VideoTemplateConfig.ARG_TEMPLATE_EXP_GRP, "");
       mTemplateEnterFrom = paramsCtrl.getString(VideoTemplateConfig.ARG_TEMPLATE_ENTER_FROM, "");
       mSceneName = paramsCtrl.getString(StudioReportConstants.ORIGINAL_OPEN_FROM, "");
       mTemplateType = paramsCtrl.getInt(VideoTemplateConfig.ARG_MATERIAL_TEMPLATE_TYPE, VideoTemplateConfig.TYPE.TEMPLATE_UNKNOW);
       mIsTemplateSupportMatting = paramsCtrl.getBoolean(VideoTemplateConfig.ARG_MATERIAL_TEMPLATE_SUPPORT_MATTING, false);
       mIsTemplateSupportBlink = paramsCtrl.getBoolean(MaterialConfig.ARG_MATERIAL_SUPPORT_BLINK, false);
       isSearch = paramsCtrl.getString(MaterialConfig.EXTRA_IS_SEARCH, "0");
       ...
   }
}

这是项目中通过 Intent 透传参数的名场面,一百行以上的参数解析代码也不再少数。

更气人的是还有与之对应的配套设施:Activity 中有几行解析代码,就有对应的几个成员变量,得把透传过来的参数存储在成员变量中,以便在跳转下一个界面时继续透传。所以下一个配套设施就是在类似gotoXXXActivity()方法中整齐划一的 put 方法:

// xxxActivity.java
private void goNext(Long materialDraftId, boolean isRoughShear) {
    final Intent intent = new Intent();
    intent.putExtra(MaterialProtocol.MATERIAL_SOURCE_KEY, mRouterFrom);
    intent.putExtra(VideoTemplateConfig.KEY_TEMPLATE_SOURCE, mTemplateFrom);
    intent.putExtra(VideoTemplateConfig.KEY_TEMPLATE_TRACE, mTemplateTrace);
    intent.putExtra(VideoTemplateConfig.ARG_MATERIAL_TEMPLATE_TAB_NAME, mCategoryName);
    intent.putExtra(VideoTemplateConfig.ARG_MATERIAL_TEMPLATE_TAB_ID, mCategoryId);
    intent.putExtra(VideoTemplateConfig.KEY_TEMPLATES_TRACE_V2, mTemplatesTraceV2);
    intent.putExtra(MaterialConfig.ARG_MATERIAL_SHOW_CURRENT_PROJECT_TAB, mShowCurrentProjectTab);
    intent.putExtra(VideoTemplateConfig.ARG_VIDEO_TEMPLATE_ID, mVideoTemplateId);
    intent.putExtra(VideoTemplateConfig.ARG_VIDEO_TEMPLATE_PATH, mVideoTemplatePath);
    intent.putExtra(VideoTemplateConfig.ARG_MATERIAL_MUSIC_INFO, mVideoTemplateMusicInfo);
    intent.putExtra(VideoTemplateConfig.ARG_MATERIAL_MUSIC_LIST, mVideoTemplateMusicList);
    intent.putExtra(VideoTemplateConfig.ARG_TEMPLATE_UP_FROM, mVideoTemplateUpFrom);
    intent.putExtra(VideoTemplateConfig.ARG_TEMPLATE_EXP_GRP, mExpGrp);
    intent.putExtra(VideoTemplateConfig.ARG_TEMPLATE_ENTER_FROM, mTemplateEnterFrom);
    intent.putExtra(StudioReportConstants.ORIGINAL_OPEN_FROM, mSceneName);
    intent.putExtra(StudioReportConstants.EXTRA_KEY_MATERIAL_DRAFT_ID, materialDraftId);
    intent.putExtra(VideoTemplateConfig.ARG_MATERIAL_TEMPLATE_TYPE, mTemplateType);
    intent.putExtra(MaterialConfig.EXTRA_IS_SEARCH, isSearch);
    ...
}

方法实在太长,只截取了部分。。。

如果透传过来的参数,在当前界面被消费过,那还气得过点。

最最最气人的就是把当前 Activity 当联邦快递,即它负责传递参数而不消费。全局搜索作为参数的成员变量,如果引用只出现了 3 次(声明,get,put),那它就是把当前界面当成了中转站。

想象这样一种场景,有一个界面是整个 App 关键路径上的必经之路,如下图中的 Activity6 所示: 微信截图_20221109214746.png

如果用上述透传参数的方式,Activity 6 必将称为“超级 Activity”,它将和所有的业务耦合,它的代码行数会很多很多、每次打开这个文件语法高亮会很慢很慢,当需要修改这个界面时你会很怕很怕、这个界面的功能衰退会很常见很常见。

但是没有办法,新增一个业务场景时,也只能新增一个成员变量,让它暂存透传过来的新参数,并在跳转的方法中把它传出去。

最绝望的是,当新增一个 Activity 时,你发现它要的一个数据在跳转链路上往前数第 4 个 Activity。这意味着你得在5个 Activity 中声明4个成员变量,调用4次put方法,以及4次get方法,才能获取想要的值。

显式向后透传

之所以会发生多层参数透传是因为,下面这些 API:

// android.app.Activity.java
public void startActivity(Intent intent) {
    this.startActivity(intent, null);
}

// android.content.Intent.java
public @NonNull Intent putExtra(String name, @Nullable Bundle value) {
    if (mExtras == null) {
        mExtras = new Bundle();
    }
    mExtras.putBundle(name, value);
    return this;
}
// android.content.Intent.java
public @Nullable Bundle getBundleExtra(String name) {
    return mExtras == null ? null : mExtras.getBundle(name);
}

即启动 Activity 的系统方法需要传递一个 Intent 对象,而该对象可以通过各种 put 方法传递参数,在接收端又有各种 get 方法获取透传参数。

少量参数在两个界面间传递使用该方案是 ok 的,但若有成批参数跨越多个界面传递还是沿用该方案就会造成极高的复杂度和耦合。

用一张图来表达这种透传方案:

微信截图_20221110152611.png

隐式向前查询

如果上述这种方式称为 “显式向后透传” 的话,下面这种方案就可以称为 “隐式向前查询”

微信截图_20221110153113.png

原本,如果 Activity 1 的业务会产生一个参数,并且 Activity 3 也需要它,Activity 1 不得不先传给 Activity 2,再透传给 Activity 3。

现在,Activity 1 先声明自己会产生参数,参数的消费方 Activity 3 不再被动地接收透传,而是主动地逐个页面地向前查询参数。当查询到 Activity 2 时没有匹配结果,就会继续往前查询,直到查询到结果为止。

为了实现这个效果得标记一个页面能生成参数:

interface Param {
    val paramMap: Map<String, Any>
}

这是一个接口,该接口持有一个属性,如果属性被这样定义在一个普通的 class 中,会出现如下报错:

微信截图_20221110154435.png IDE 提示属性必须被初始化或者抽象化。

而定义在接口中的属性默认是抽象的,所以可以省去 abstract 关键词。

当实现带有抽象属性的接口时,得为属性定义 get/set 方法:

class Activity1 : AppCompatActivity(), Param{
    override val paramMap: Map<String, Any>
        get() =  mapOf( "type" to  1 )
}

override关键词表示重写一个抽象属性,因为属性是val,它不可以被改变,所以只需要定义一个get()方法就好,即定义如何获取该属性。

界面生产参数的方式可能是多种多样的,上述的例子中,参数是一个常量 1,如果参数是变量也是 ok 的:

class Activity1 : AppCompatActivity(), Param{
    private var materialType:Int = 0
    override val paramMap: Map<String, Any>
        get() =  mapOf( "type" to  materialType )
}

当界面声明自己能生产参数之后,就什么事情也不用做了,它不用知道该把这个参数传递给哪个后续界面(这是一种解耦)。

有了参数生成能力,下一步就是参数获取能力。得在 Activity 层面方便的获取前序界面生成的参数。这相当于为 Activity 扩展一种新能力。Kotlin 中的 “扩展方法” 正适用于该场景:

fun <T> Activity.getParam(key: String): T {}

类名.方法名()这样的语法表示为 Activity 的实例扩展一个方法,该方法需输入一个 key 参数表示键,返回值是一个泛型表示值。

要获取前序界面生成的参数,就得先获取前序界面的实例。

系统帮我们维护了一个 Activity 栈,网上搜索了一下,只能找到下面这个方法:

val ams = getSystemService(Context.ACCESSIBILITY_SERVICE) as ActivityManager
val tasks = ams.getRunningTasks(10)
val iterator = tasks.iterator()
while (iterator.hasNext()){
    val taskInfo = iterator.next() as RunningTaskInfo
    taskInfo.topActivity
}

该方法槽不能满足当前需求,首先它只能获取 task 栈顶的 Activity,其次 ActivityManager.getRunningTask() 已经废弃了。

遂只能自己维护 Activity 栈:

object PageStack : Application.ActivityLifecycleCallbacks {
    val stack = LinkedList<Activity>()
    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
        stack.add(activity)
    }
    override fun onActivityDestroyed(activity: Activity) {
        stack.remove(activity)
    }
    override fun onActivityStarted(activity: Activity) {}
    override fun onActivityResumed(activity: Activity) {}
    override fun onActivityPaused(activity: Activity) {}
    override fun onActivityStopped(activity: Activity) {}
    override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
}

Application.ActivityLifecycleCallbacks是一个全局 Activity 生命周期监听器。

当任意一个 Activity 创建的时候,把它追加到自定义的栈结构,当任意一个 Activity 销毁时,把它从栈顶移除。

在 Kotlin 中object保留词可用于快速单例。这种语法称为对象声明

对象声明将类声明和该类的单一实例声明结合到了一起。与普通类一样,一个对象声明也可以包含任何属性、方法、初始化语句块,等等。唯一与普通类的实例不同的是,对象声明在定义的时候就立刻创建了实例。

对象声明也有局限性,它不能自定义构造方法,所以也无法进行构造参数注入。

PageStack 是一个对象声明,全局只有一个实例,这样可以方便地在任何地方获取它。

然后在 Application 中注册之

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        registerActivityLifecycleCallbacks(PageStack)
    }
}

下面就可以来定义获取前序页面参数的方法了:

fun <T> Activity.getParam(key: String): T {
    // 获取 Activity 栈的迭代器
    val iterator = PageStack.stack.let { it.listIterator(it.size) }
    // 从后往前迭代
    while (iterator.hasPrevious()) {
        // 获取上一个 Activity 结点
        val activity = iterator.previous()
        // 如果 Activity 携带参数,则根据 key 获取参数
        (activity as? Param)?.paramMap?.getOrDefault(key, null)?.also { return it as
    }
    throw IllegalArgumentException("missing Parameter for the previous Activity/Fragment")
}

使用迭代器ListIterator可以方便的实现从后向前的遍历。它提供了配套的hasPrevious()previous()方法。

从后向前获取 Activity 实例之后使用as?操作符将其强转为Param接口(强转失败时会返回 null,后续逻辑不再执行),若强转成功则表示当前 Activity 能生成参数,此时在 Map 上进行键值匹配。

当遍历完所有 Activity 都未找到匹配值,则直接抛异常用于提醒上层一次可能的值漏传。另外,调用该方法需传入泛型以指定参数类型,Param 接口中参数以 Any 类型存储,获取参数时会根据指定参数机型强转。若强转失败则会抛异常,用于提醒错误的类型变换。

然后就可以像这样重构参数透传:

class Activity1 : AppCompatActivity(), Param{
    private var materialType:Int = 0
    // 定义当前 Activity 能生成的参数
    override val paramMap: Map<String, Any>
        get() =  mapOf( "type" to  materialType )
}

class Activity3 : AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        getParam<Int>("type") // 获取 Activity1 的参数
    }
}

如果参数不是在 Activity 层面产生,而是在 子 Fragment 怎么处理?

微信截图_20221110164912.png

对于透传方案来说,只是把 put 参数的地方从 Activity 换成 Fragment 而已。

但向前查询方案,还不能很好的 cover 这种 case。因为约定能生成参数的只有 Activity 并且向前查询的时候,只会往前查 Activity。

那把 Fragment 生成的参数上提到 Activity 层?

可以是可以,但这样会产生不必要的耦合,Activity 不该了解内部 Fragment 的细节。

更好的方案是,让 Fragment 也能生成参数。

class Fragment1Fragment(), Param{
    private var materialType:Int = 0
    override val paramMap: Map<String, Any>
        get() =  mapOf( "type" to  materialType )
}

那除了维护 Activity 的栈,还得维护与 Activity 对应的 Fragment 集合:

object PageStack : Application.ActivityLifecycleCallbacks {
    val stack = LinkedList<Activity>()
    // Fragment 集合
    val fragments = hashMapOf<Activity, MutableList<Fragment>>()
    // Fragment 生命周期监听器
    private val fragmentLifecycleCallbacks by lazy(LazyThreadSafetyMode.NONE) {
        object : FragmentManager.FragmentLifecycleCallbacks() {
            override fun onFragmentViewCreated(fm: FragmentManager, f: Fragment, v: View, savedInstanceState: Bundle?) {
                // 当 Fragment 被创建后,把它和相关的 activity 存入 map
                f.activity?.also { activity ->
                    fragments[activity]?.also { it.add(f) } ?: run { fragments[activity] = mutableListOf(f) }
                }
            }
        }
    }
    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
        stack.add(activity)
        // 注册 Fragment 生命周期监听器
        (activity as? FragmentActivity)?.supportFragmentManager?.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
    }
    override fun onActivityDestroyed(activity: Activity) {
        stack.remove(activity)
        // 移除 Fragment 生命周期监听器
        (activity as? FragmentActivity)?.supportFragmentManager?.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks)
        // 清空 Fragment 集合
        fragments[activity]?.clear()
        fragments.remove(activity)
    }
    override fun onActivityStarted(activity: Activity) {}
    override fun onActivityResumed(activity: Activity) {}
    override fun onActivityPaused(activity: Activity) {}
    override fun onActivityStopped(activity: Activity) {}
    override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
}

为 PageStack 新增成员变量,用于维护 Activity 对应的 Fragment 集合。采用 HashMap 为存储结构,键为 Activity 实例,值为该 Activity 对应的 Fragment 集合。

当 Activity 被创建时,为该 Activity 的 FragmentManager 注册 Fragment 生命周期观察者。当 Activity 被销毁时,注销对应的 Fragment 生命周期观察者。并清空 Fragment 集合以避免内存泄漏。

相应的,查询参数的逻辑也得稍作修改:

fun <T> Activity.getParam(key: String): T {
    val iterator = PageStack.stack.let { it.listIterator(it.size) }
    while (iterator.hasPrevious()) {
        val activity = iterator.previous()
        // 先查询 Activity 层级是否存在参数
        (activity as? Param)?.paramMap?.getOrDefault(key, null)?.also { return it as T }
        // 若 Activity 层级查询失败,继续查询该页面的所有 Fragment
        val paramFragment = PageStack.fragments[activity]?.firstOrNull { (it as? Param)?.paramMap?.getOrDefault(key, null) != null }
        if(paramFragment !=null) return (paramFragment as Param).paramMap[key] as T
    }
    throw IllegalArgumentException("missing Parameter for the previous Activity/Fragment")
}

重复 key 冲突

该方案有一个痛点,如果前序界面中有两个生产数据的界面使用了相同的 key,则向前查询时会命中离查询结点最近的那个。亦或是前序界面中多个平级的 Fragment 生成参数时使用了相同的 key 会有同样的问题。

我想到的解决方案是: 在获取数据时检测键,若遇到相同键则抛异常。

fun <T> Activity.getParam(key: String): T {
    val iterator = PageStack.stack.let { it.listIterator(it.size) }
    // 记录获取的值
    var value: T? = null
    while (iterator.hasPrevious()) {
        val activity = iterator.previous()
        val v = (activity as? Param)?.paramMap?.getOrDefault(key, null)
        if (v != null) {
            if (value == null) {
                value = v as T // 记录值
            }
            // 在 Activity 中发现重复 key
            else {
                throw IllegalArgumentException("duplicated key=${key} in previous ${activity.javaClass.simpleName}")
            }
        }
        val paramFragments = PageStack.fragments[activity]?.filter { (it as? Param)?.paramMap?.getOrDefault(key, null) != null }
        // 在 Fragment 中发现重复 key
        if (paramFragments?.size.orZero > 1) throw IllegalArgumentException("duplicated key=${key} in previous fragments=${paramFragments?.print { it.javaClass.simpleName }}")
        else if (paramFragments?.size.orZero == 1) {
            if (value == null) {
                // 记录值
                value = (paramFragments?.first() as? Param)?.paramMap?.get(key) as? T
            } else {
                throw IllegalArgumentException("duplicated key=${key} in previous ${paramFragments?.first()?.javaClass?.simpleName}")
            }
        }
    }
    return value ?: throw IllegalArgumentException("missing Parameter for key=$key the previous Activity/Fragment")
}

之前的方案是获取到值就直接返回,而现在是把值记录在 value 中,然后继续遍历所有前序结点,若再次找到相同的键,则抛出异常。

其中的print()是打印列表的扩展方法,详细分析可以点击每次调试打印日志都很头痛

被销毁的Activity

该方案还有一个痛点:如果前序界面被销毁,它生成的参数是不是也同归于尽了?

是的!但是有补救方法!

通过 ActivityLifecycleCallbacks 可以监听到任何 Activity 的销毁,如果销毁的 Activity 实现了 Param 接口,此时就能将其参数保存下来:

object PageStack : Application.ActivityLifecycleCallbacks {
    val stack = LinkedList<Activity>()
    // 存放销毁界面的参数
    val destroyMap = mutableMapOf<String,Any>()
    override fun onActivityDestroyed(activity: Activity) {
        stack.remove(activity)
        (activity as? FragmentActivity)?.supportFragmentManager?.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks)
        fragments[activity]?.clear()
        fragments.remove(activity)
        // 若被销毁的界面能生成参数,则暂存到destroyMap中
        (activity as? Param)?.also { 
            it.paramMap.forEach { entry -> destroyMap[entry.key] = entry.value }
        }
    }
}

在消费参数时,若未能从 Activity 或 Fragment 中找到对应 key,则尝试在 destroyMap 中寻找:

fun <T> Activity.getParam(key: String): T {
    val iterator = PageStack.stack.let { it.listIterator(it.size) }
    var value: T? = null

    while (iterator.hasPrevious()) {
        val activity = iterator.previous()
        //1. 在activity 中寻找
        val v = (activity as? Param)?.paramMap?.getOrDefault(key, null)
        if (v != null) {
            if (value == null) {
                value = v as? T
            } else {
                throw IllegalArgumentException("duplicated key=${key} in previous ${activity.javaClass.simpleName}")
            }
        }
        // 2. activity 中未找到参数则尝试在 fragment 中寻找
        val paramFragments = PageStack.fragments[activity]?.filter { (it as? Param)?.paramMap?.getOrDefault(key, null) != null }
        if (paramFragments?.size.orZero > 1) throw IllegalArgumentException("duplicated key=${key} in previous fragments=${paramFragments?.print { it.javaClass.simpleName }}")
        else if (paramFragments?.size.orZero == 1) {
            if (value == null) {
                value = (paramFragments?.first() as? Param)?.paramMap?.get(key) as? T
            } else {
                throw IllegalArgumentException("duplicated key=${key} in previous ${paramFragments?.first()?.javaClass?.simpleName}")
            }
        }
    }
    // 3. 若 acitivty 和 fragment 中都未找到,最后尝试在 destroy 中寻找
    if(value == null){
        value = PageStack.destroyMap.getOrDefault(key,null) as T
    }
    return value ?: throw IllegalArgumentException("missing Parameter for key=$key the previous Activity/Fragment")
}

总结

通过将“显式向后透传参数”转变为“隐式向前查询参数”完全避免了界面间的参数透传,使得各界面间更加耦合,参数更容易维护。

同时通过 Kotlin 的抽象属性,扩展方法,对象声明、类型转换 as? 这些语法糖将向前查询参数的复杂度隐藏起来,使得上层能以最简洁的方式查询参数。

推荐阅读

业务代码参数透传满天飞?(一)

业务代码参数透传满天飞?(二)

全网最优雅安卓控件可见性检测

全网最优雅安卓列表项可见性检测

页面曝光难点分析及应对方案

你的代码太啰嗦了 | 这么多对象名?

你的代码太啰嗦了 | 这么多方法调用?