业务代码参数透传满天飞?
Posted 上马定江山
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了业务代码参数透传满天飞?相关的知识,希望对你有一定的参考价值。
引子
项目中参数多级透传满天飞的情况很常见,增加了开发的复杂度、出错的可能、及维护的的难度。
透传包括两种形式:
- 不同界面之间参数透传。
- 同一界面中不同层级控件间透传。
该系列的目标是消除这两种参数透传,使得不同界面以及同一界面内各层级间更加解耦,降低参数传递开发的复杂度,减少出错的可能,增加可维护性。
上一篇通过向前查询参数的方式解决了第一个 case,本篇先聚焦在第二个 case,即同一界面不同层级控件间的参数透传。
透传举例
比如下面这个场景:
特效卡片的点击事件需要传入私参“type”,以表示属于哪个 tab 页。
界面层级如下:素材集市用 EffectActivity 来承载,其中的标签栏下方是一个子 Fragment ,其中包含了 ViewPager 控件,该控件内部的每一个页又是一个 Fragment。
埋点私参和上报时机分处于两个不同的页面层级。上报时机在最内层 Fragment 触发,而私参在 Activity 层级生成,遂需通过两层 Fragment 的透传。
于是就会出现如下代码:
// EffcetActivity.kt
override fun showEffectListContent(index: Int, from: String?)
mVpEffectContent.adapter = SimpleFragmentStatePagerAdapter(supportFragmentManager).apply
mTitles = arrayOf("视频库", "音乐", "音效", "贴纸", "转场", "特效", "滤镜", "背景", "字幕", "字体")
mCount = mTitles!!.size
createFragment = position ->
when (position)
0 ->
val fragment = RemoteCenterFragment.newInstance( -1, true, 0, MaterialProtocol.SOURCE.MATERIAL_MARKET, 1, from, 0, 0)
val paramsParser = DeepLinkParamsParser(compileDeepLinkParams())
(fragment as IDeepLinkPage).setDeepLinkParams(paramsParser.deeplinkParams)
fragment
1 ->
MaterialMusicFragment.newInstance(from,mSelectedTabId,mSelectedModelId)
2 ->
MaterialAudioFragment.newInstance(mSelectedTabId,mSelectedModelId)
// 索引值到类型值的映射
3 ->
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_VSTICKER)
4 ->
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_TRANSITION)
5 ->
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_EFFECT)
6 ->
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_FILTER)
7 ->
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_BACKGROUND)
8 ->
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_SUBTITLE)
else ->
EffectListFragment.newInstance(CommonConstant.SERVER.TYPE_FONT)
mVpEffectContent.currentItem = index
mVpEffectContent.offscreenPageLimit = 10
mTlEffectTabs.setupWithViewPager(mVpEffectContent)
EffectListFragment
即使承载 ViewPager 的 Fragment,上述代码在构建其实例时做了分类讨论,目的是为了根据不同类型的 tab 透传相应的 type 值。
EffectListFragment 不得不先接受透传参数并继续传递到下一个层级:
class EffectListFragment : BaseMvpFragment
// 保存透传参数的变量
private var mCurrentType: Int = -1
override fun initConfig(savedInstanceState: Bundle?)
// 获取透传参数
mCurrentType = arguments?.getInt(CommonConstant.EFFECTCENTER.TYPE) ?: 0
selectHotTab()
showEffectDetails()
override fun showEffectDetails()
mVpEffectDetails?.adapter = SimpleFragmentPagerAdapter(childFragmentManager).apply
mCount = 1
createFragment = position ->
// 参数继续透传到下一个层级
EffectDetailsFragment.newInstance(mCurrentType, CommonConstant.EFFECTCENTER.ORDER_HOT).also
currentFragments[0] = it
companion object
fun newInstance(type: Int): EffectListFragment
// 参数透传
return EffectListFragment().apply
arguments = Bundle().apply putInt(CommonConstant.EFFECTCENTER.TYPE, type)
最后接受并消费透传 type 的是 EffectDetailsFragment,即纵向滚动列表的承载页:
class EffectDetailsFragment : BaseMvpFragment<EffectDetailsContract.IView, EffectDetailsPresenter>()
// 保存透传参数的变量
private var mCurrentType: Int = 0
override fun initConfig(savedInstanceState: Bundle?)
// 接受透传参数
mCurrentType = arguments?.getInt(CommonConstant.EFFECTCENTER.TYPE) ?: 0
mOrder = arguments?.getInt(ORDER) ?: 0
initDetailsContent()
override fun initEvent()
mTvNetworkRetry.setOnClickListener(this)
mEffectDetailsAdapter.addOnItemClickListener(object : EffectDetailsAdapter.OnItemClickListener
override fun onItemClick(entity: EffectDataEntity?)
entity?.let
// 消费透传参数,上报埋点
StudioReport.reportClickAlbumClick(it.type, it.id, mOrder, "all", mCurrentType)
)
companion object
const val ORDER: String = "order"
fun newInstance(type: Int, order: Int): EffectDetailsFragment
return EffectDetailsFragment().apply
// 接收透传
arguments = Bundle().apply
putInt(CommonConstant.EFFECTCENTER.TYPE, type)
putInt(ORDER, order)
整个界面层级以及参数传递路径如下:
Activity 中有一个 Fragment,而它内部又嵌套了一个 Fragment。
中间的 Fragment 很无辜,因为它并不需要消费 type 参数,而只是做一个快递员。
当前只有两层,如果层级再增多,因此而增加的复杂度和工作量让人难以接受。
向上查询
如果把上述传参的方式叫做 “自顶向下透传” 的话,下面要介绍的这个方案可以称为 “自底向上查询”。
自顶向下透传是容易实现的,因为父亲总是持有孩子的引用,向孩子注入参数轻而易举。
有没有一种方案可以实现反向的参数查询,即当孩子触发埋点事件时,逐级向上查询父亲生成的参数。
android 中的控件是持有父亲的:
// android.view.View.java
public final ViewParent getParent()
return mParent;
通过一个循环不停地获取当前控件的父控件,就能从 View 树的叶子结点遍历到树根:
var viewParent: View?
do
viewParent = viewParent?.parent as? View
while(viewParent != null)
对于 Activity 来说,树根就是 DecorView。对于 Fragment 来说,树根就是 onCreateView() 中创建的视图。
Fragment 最终会以一个 View 的形式嵌入到 Activity 的 View 树中。所以对于当个 Activity 来说,不管嵌套几层 Fragment,其视图结构最终都可以归为一棵 View 树。
如何让 Activity View 树中的每一个控件都能携带业务参数?
需要定义一个接口:
// 可跟踪的结点
interface TrackNode
fun fillTrackParams(): HashMap<String, String>?
为 View 新增一个扩展属性,让每个控件都持有一个 TrackNode:
var View.trackNode: TrackNode?
get() = this.getTag(R.id.spm_id_tag) as? TrackNode
set(value)
this.setTag(R.id.spm_id_tag, value)
将携带参数的能力存放在 View.tag 中,这样任何控件都可以携带参数了。
让 Activity 携带参数体现为让其根视图 DecorView 携带参数:
// 在所有 Activity 的基类中实现 TrackNode,则所有 Activity 都具备了携带参数的能力
open class BaseActivity : AppCompatActivity(), TrackNode
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
// Activity 携带参数表现为其根视图携带参数
window.decorView.rootView.trackNode = this
override fun fillTrackParams(): HashMap<String, String>?
return null
同样地,Fragment 也有类似的实现:
open class BaseFragment : Fragment(), TrackNode
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
super.onViewCreated(view, savedInstanceState)
// Fragment 携带参数表现为其根视图携带参数
getView()?.trackNode = this
override fun fillTrackParams(): HashMap<String, String>?
return null
这样一来,一个窗口中整个 View 树的任何一个结点都具备了携带参数的能力,从最顶层的 Activity,到其内部的 Fragment,再到任何一个控件。参数就不再需要自顶向下透传,而是可以自底向上查询:
fun View.getTrackNode(): HashMap<String, String>
val map = hashMapOf<String, String>()
// 获取当前结点的参数
trackNode?.fillTrackParams()?.also map.putAll(it)
// 不断获取父亲以向上查询
var viewParent = parent as? View
do
// 查询父控件是否携带参数
val info = viewParent?.trackNode?.fillTrackParams()
// 若父控件携带参数则将其拼接
info?.also map.putAll(it)
// 继续获取父控件
viewParent = viewParent?.parent as? View
while (viewParent != null) // 直到回溯到了整个界面的根视图
return map
为 View 自定义了一个扩展方法,该方法返回一个 Map,该 Map 中包含了从当前界面向上到树根整个链路中所有携带的参数集合。
重构透传
先在 Activity 层级将标签页的 type 拼接到 TrackNode 中,而不是作为参数传递给 EffectListFragment:
// EffectActivity.kt
override fun showEffectListContent(index: Int, from: String?)
mVpEffectContent.adapter = SimpleFragmentStatePagerAdapter(supportFragmentManager,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT).apply
mTitles = arrayOf("视频库", "音乐", "音效", "贴纸", "转场", "特效", "滤镜", "背景", "字幕", "字体")
mCount = mTitles!!.size
createFragment = position ->
when (position)
0 ->
val fragment = RemoteCenterFragment.newInstance(-1,true,0,MaterialProtocol.SOURCE.MATERIAL_MARKET,1,from,0,0)
val paramsParser = DeepLinkParamsParser(compileDeepLinkParams())
(fragment as IDeepLinkPage).setDeepLinkParams(paramsParser.deeplinkParams)
fragment
1 -> MaterialMusicFragment.newInstance(from, mSelectedTabId, mSelectedModelId)
2 -> MaterialAudioFragment.newInstance(mSelectedTabId, mSelectedModelId)
// 可以无差别地构造 EffectListFragment 实例
else -> EffectListFragment.newInstance()
mVpEffectContent.currentItem = index
mVpEffectContent.offscreenPageLimit = 10
mTlEffectTabs.setupWithViewPager(mVpEffectContent)
// 索引和常量的映射
private val tabMap = mapOf(
3 to CommonConstant.SERVER.TYPE_VSTICKER,
4 to CommonConstant.SERVER.TYPE_TRANSITION,
5 to CommonConstant.SERVER.TYPE_EFFECT,
6 to CommonConstant.SERVER.TYPE_FILTER,
7 to CommonConstant.SERVER.TYPE_BACKGROUND,
8 to CommonConstant.SERVER.TYPE_SUBTITLE,
9 to CommonConstant.SERVER.TYPE_FONT,
)
// Activity 层级的参数拼接
override fun fillTrackParams(): HashMap<String, String>?
// 只拼接当前显示页的常量
return hashMapOf("type" to tabMap[mVpEffectContent.currentItem].toString()
第二个层级的 Fragment 在构建实例时不再接受传参(更加单纯):
class EffectListFragment : BaseFragment()
companion object
// 没有参数传入的构造方法
fun newInstance(): EffectListFragment = EffectListFragment()
在最内层的 Fragment 消费参数:
class EffectDetailsFragment : BaseFragment()
// 向上查参
private val type: Int
get() = view?.getTrackNode()?.getOrElse("type") "" ?.safeToInt() ?: 0
override fun initEvent()
mEffectDetailsAdapter.addOnItemClickListener(object : EffectDetailsAdapter.OnItemClickListener
override fun onItemClick(entity: EffectDataEntity?)
// 消费参数进行埋点
entity?.let
ReportUtil.reportClick(it.id, type)
)
companion object
// 没有type 传入的构造方法
fun newInstance(): EffectDetailsFragment = EffectDetailsFragment()
消费参数时不再通过上一个界面透传,而是通过自底向上的查询。
因为约定的参数是 HashMap<String, String> 类型的,而消费的参数是 Int 类型的所以得进行类型转换
如果强制的使用如下方式进行转换,则可能发生运行时崩溃,比如下面这个场景:
" " as Int
为了避免这类崩溃,有必要做一个统一处理:
fun String?.safeToInt(): Int = this?.let
try
Integer.parseInt(this)
catch (e: NumberFormatException)
e.printStackTrace()
0
?: 0
为 String 定义一个扩展方法,该方法返回 Int 值,在内部调用Integer.parseInt(this)
将当前的 String 转换为 Int,并在其外层包裹了 try-catch 以捕获非数字字串转换异常的情况。
使用 Kotlin 中的预定义let()
方法配合try-catch
表达式以及 Evis 运算符,让这个方法的表达异常简洁。
其中let()
的定义如下:
public inline fun <T, R> T.let(block: (T) -> R): R
contract
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
return block(this)
let 也是一个扩展方法,被扩展对象是泛型,表示它可以被任何对象调用。let 接收一个 lambda,该 lambda 会将类型 T 变换为 类型 R,let 方法内部只是通过block(this)
执行了该 lambda 并返回,遂 let 的返回值即是 lambda 的值(lambda 最后一条语句的值)。
从 let 的定义可以看出,它通常用于将一个对象转换为另一个对象。当前场景中它被用于将 String 转换为 Int。
String 转换为 Int 是可能抛异常的,遂用 try-catch 包裹之。Kotlin 中try-catch
是一个表达式,它是有值的,等于每个分支最后一条语句的值。这个特性使得不必多声明一个局部变量:
int result = 0;
try
result = Integer.parseInt(str);
catch (Exception e)
result = -1
return result;
所以整个 safeToInt() 的返回值是 let 的返回,而 let 的返回值是 try-catch 的返回值。
最后因为被扩展的对象是 String?,所以返回值是可空的,方法内部通过?:
处理了这种情况。表达式1 ?: 表达式2
意思是当表达式1为空时,执行表达式2。
适用场景
自底向上查询参数方案适用于同一窗口的任何层级之间的参数传递。
当在 Fragment 中向上查询时,要在onCreateView()
之后,因为在此之前,Fragment 的视图层级还未生成,getView()
会返回 null。
RecyclerView 中 ItemView 无法使用自底向上查询,因为ItemView.parent
为空。
可以在 inflate ItemView 布局时将 attachToRoot 设置为 true:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
val itemView = LayoutInflater.from(parent.context).inflate(
R.layout.material_item_effect_details,
parent,
true)// 将 attachToRoot 设置为 true
return ViewHolder(itemView)
这样 ItemView 的 parent 就不为空了,但是 ItemView 的 LayoutParam 就会被其父控件的 LayoutParam 覆盖,使得 ItemView 的布局样式不符合预期。
列表项参数透传解决方案
那列表相关的参数透传路径就一定得是 Activity -> Adapter -> ViewHolder ?
Adapter 的语义是完成数据到视图的转换。ViewHolder 的语义是描述如何构建表项视图及其交互。
如果将透传逻辑和表项的构建及交互逻辑耦合在一起,除了增加了透传参数的复杂度,还使得后者无法被独立复用。
更好的做法是将表项的曝光和点击事件上移到 Activity/Fragment 处理,为此新增了两个扩展法方法:
fun RecyclerView.setOnItemClickListener(listener: (View, Int, Float, Float) -> Boolean)
addOnItemTouchListener(object : RecyclerView.OnItemTouchListener
val gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener
override fun onShowPress(e: MotionEvent?)
override fun onSingleTapUp(e: MotionEvent?): Boolean
e?.let
findChildViewUnder(it.x, it.y)?.let child ->
val realX = if (child.left >= 0) it.x - child.left else it.x
val realY = if (child.top >= 0) it.y - child.top else it.y
return listener( child, getChildAdapterPosition(child), realX, realY )
return false
override fun onDown(e: MotionEvent?): Boolean
return false
override fun onFling( e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float ): Boolean
return false
override fun onScroll( e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float ): Boolean
return false
override fun onLongPress(e: MotionEvent?)
)
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent)
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean
gestureDetector.onTouchEvent(e)
return false
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean)
)
通过判断触点坐标落在 RecyclerView 的哪个孩子上进行点击事件的回调。详细分析可以点击读源码长知识 | 更好的 RecyclerView 表项点击监听器
以及 RecyclerView 表项百分比曝光扩展方法:
fun RecyclerView.onItemVisibilityChange(percent: Float = 0.5f, block: (itemView: View, adapterIndex: Int, isVisible: Boolean) -> Unit)
val rect = Rect() // reuse rect object rather than recreate it everytime for a better performance
val visibleAdapterIndexs = mutableSetOf<Int>()
val scrollListener = object : OnScrollListener()
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int)
super.onScrolled(recyclerView, dx, dy)
// iterate all children of RecyclerView to check whether it is visible
for (i in 0 until childCount)
val child = getChildAt(i)
val adapterIndex = getChildAdapterPosition(child)
val childVisibleRect = rect.also child.getLocalVisibleRect(it)
val visibleArea = childVisibleRect.let it.height() * it.width()
val realArea = child.width * child.height
if (visibleArea >= realArea * percent)
if (visibleAdapterIndexs.add(adapterIndex))
block(child, adapterIndex, true)
else
if (adapterIndex in visibleAdapterIndexs)
block(child, adapterIndex, false)
visibleAdapterIndexs.remove(adapterIndex)
addOnScrollListener(scrollListener)
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener
override fun onViewAttachedToWindow(v: View?)
override fun onViewDetachedFromWindow(v: View?)
if (v == null || v !is RecyclerView) return
if (ViewCompat.isAttachedToWindow(v))
v.removeOnScrollListener(scrollListener)
removeOnAttachStateChangeListener(this)
)
通过监听列表滚动事件,并在其中遍历列表所有的孩子,同时计算每个孩子矩形区域在列表中展示的百分比判断其可见性,详细分析可以点击
总结
通过思路的转变,将“自顶向下透传参数”转变为“自顶向上查询参数”,降低了同一界面层级中各控件之间的耦合,使得每个控件都更加单纯。
推荐阅读
作者:唐子玄
链接:https://juejin.cn/post/7165427216589783076
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
一、架构师筑基必备技能
1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……
二、Android百大框架源码解析
1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程
三、Android性能优化实战解析
- 腾讯Bugly:对字符串匹配算法的一点理解
- 爱奇艺:安卓APP崩溃捕获方案——xCrash
- 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
- 百度APP技术:Android H5首屏优化实践
- 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
- 携程:从智行 Android 项目看组件化架构实践
- 网易新闻构建优化:如何让你的构建速度“势如闪电”?
- …
四、高级kotlin强化实战
1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》
-
从一个膜拜大神的 Demo 开始
-
Kotlin 写 Gradle 脚本是一种什么体验?
-
Kotlin 编程的三重境界
-
Kotlin 高阶函数
-
Kotlin 泛型
-
Kotlin 扩展
-
Kotlin 委托
-
协程“不为人知”的调试技巧
-
图解协程:suspend
五、Android高级UI开源框架进阶解密
1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
六、NDK模块开发
1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习
七、Flutter技术进阶
1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)
…
八、微信小程序开发
1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……
全套视频资料:
一、面试合集
二、源码解析合集
三、开源框架合集
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓
以上是关于业务代码参数透传满天飞?的主要内容,如果未能解决你的问题,请参考以下文章