この記事は、レア アース ナゲット テクノロジー コミュニティの最初の署名付き記事です。14 日以内の転載は禁止されています。14 日を過ぎると無断転載は禁止されます。侵害は調査する必要があります。
プライマー
プロジェクトでは、パラメータのマルチレベルの透過的な伝送が空を飛び交うことは非常に一般的であり、開発の複雑さ、エラーの可能性、およびメンテナンスの困難さが増します。
透過的な送信には、次の 2 つの形式があります。
- 異なるインターフェース間のパラメータの透過的な伝送。
- 同じインターフェース内の異なるレベルのコントロール間の透過的な伝送。
このシリーズの目標は、これら 2 種類のパラメーターの透過的な送信を排除し、同じインターフェイス内の異なるインターフェイスとレイヤーをより分離し、パラメーター送信開発の複雑さを軽減し、エラーの可能性を減らし、保守性を高めることです。
この記事では、まず最初のケース、つまり、異なるインターフェイス間でのパラメーターの透過的な送信に焦点を当てます。
// 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 を介してパラメータが透過的に送信されるプロジェクトの有名なシーンであり、100 行を超えるパラメータ解析コードも少なくありません。
さらにイライラするのは、対応するサポート機能があることです: アクティビティには数行の解析コードがあり、対応するメンバー変数がいくつかあります. インターフェイスを通過し続けます. したがって、次のサポート機能は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);
...
}
复制代码
メソッドが長すぎます。一部しかインターセプトされていません。. .
透過的に渡されたパラメーターが現在のインターフェイスで消費されている場合、私は少し腹を立てます。
最も苛立たしいことは、現在のアクティビティを FedEx として扱うことです。つまり、消費せずにパラメータを渡す責任があります。メンバー変数をパラメータとしてグローバルに検索し、参照が 3 回 (宣言、get、put) しか表示されない場合は、現在のインターフェイスを転送ステーションと見なします。
次の図の Activity6 に示すように、アプリ全体のクリティカル パスで渡す必要があるインターフェイスがあるシナリオを想像してください。
上記のパラメータを透過的に渡す方法を使えば、Activity 6は間違いなく「スーパーアクティビティ」と呼ばれ、すべてのビジネスに結合され、コードの行数は非常に多くなり、シンタックスハイライトはこのファイルを開くたびに非常に遅く、このインターフェイスを変更する必要がある場合、非常に恐れ、このインターフェイスの機能低下は非常に一般的であり、非常に一般的です。
ビジネスシナリオを追加する場合、メンバ変数を追加して、渡された新しいパラメータを一時的に格納し、それを jump メソッドで渡すしかありません。
最绝望的是,当新增一个 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 Fragment1:Fragment(), 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? 这些语法糖将向前查询参数的复杂度隐藏起来,使得上层能以最简洁的方式查询参数。