Transparent transmission of business code parameters everywhere? (one)

This article is the first signed article of the Rare Earth Nuggets Technology Community. Reprinting is prohibited within 14 days. Reprinting without authorization is prohibited after 14 days. Infringement must be investigated!

Primer

It is very common for multi-level transparent transmission of parameters to fly all over the sky in projects, which increases the complexity of development, the possibility of errors, and the difficulty of maintenance.

Transparent transmission includes two forms:

  1. Transparent transmission of parameters between different interfaces.
  2. Transparent transmission between controls of different levels in the same interface.

The goal of this series is to eliminate these two kinds of parameter transparent transmission, make different interfaces and layers in the same interface more decoupled, reduce the complexity of parameter transmission development, reduce the possibility of errors, and increase maintainability.

This article first focuses on the first case, that is, the transparent transmission of parameters between different interfaces:

// 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");
       ...
   }
}
复制代码

This is a famous scene in the project where parameters are transparently transmitted through Intent, and there are not a few parameter parsing codes with more than one hundred lines.

What's even more irritating is that there are corresponding supporting facilities: there are several lines of parsing code in the Activity, and there are several corresponding member variables. Continue to pass through the interface. So the next supporting facility is gotoXXXActivity()the put method that is uniform among similar methods:

// 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);
    ...
}
复制代码

The method is too long, only part of it is intercepted. . .

If the transparently passed parameters have been consumed in the current interface, then I will be a little angry.

The most irritating thing is to treat the current Activity as FedEx, that is, it is responsible for passing parameters without consumption. Globally search the member variable as a parameter. If the reference appears only 3 times (declaration, get, put), then it regards the current interface as a transfer station.

Imagine a scenario where there is an interface that must be passed on the critical path of the entire App, as shown in Activity6 in the figure below:微信截图_20221109214746.png

If the above method of transparently passing parameters is used, Activity 6 will definitely be called a "super activity", which will be coupled with all businesses, and the number of lines of code will be many, many, and the syntax highlighting will be very slow every time this file is opened , when you need to modify this interface, you will be very afraid, and the function decline of this interface will be very common and very common.

But there is no way. When adding a business scenario, you can only add a member variable, let it temporarily store the new parameter passed through, and pass it out in the jump method.

最绝望的是,当新增一个 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")
}
复制代码

总结

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

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

Guess you like

Origin juejin.im/post/7165427212911378445