本文为稀土技术社区首发签约文章,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 所示:

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

假如用上述透传参数的办法,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 的,但若有成批参数跨过多个界面传递仍是沿袭该计划就会形成极高的复杂度和耦合。

用一张图来表达这种透传计划:

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

隐式向前查询

假如上述这种办法称为 “显式向后透传” 的话,下面这种计划就能够称为 “隐式向前查询”

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

原本,假如 Activity 1 的事务会发生一个参数,而且 Activity 3 也需求它,Activity 1 不得不先传给 Activity 2,再透传给 Activity 3。

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

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

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

这是一个接口,该接口持有一个特点,假如特点被这样界说在一个一般的 class 中,会出现如下报错:

业务代码参数透传满天飞?(一)
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 怎么处理?

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

对于透传计划来说,仅仅把 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()是打印列表的扩展办法,详细剖析能够点击每次调试打印日志都很头痛

总结

经过将“显式向后透传参数”转变为“隐式向前查询参数”彻底防止了界面间的参数透传,使得各界面间愈加耦合,参数更容易保护。

一起经过 Kotlin 的笼统特点,扩展办法,目标声明、类型转化 as? 这些语法糖将向前查询参数的复杂度躲藏起来,使得上层能以最简练的办法查询参数。

引荐阅览

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

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

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

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

页面曝光难点剖析及应对计划

你的代码太啰嗦了 | 这么多目标名?

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