尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

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

业务代码参数透传满天飞?(二) 引子项目中参数多级透传满天飞的情况很常见增加了开发的复杂度、出错的可能、及维护的的难度。透传包括两种形式不同界面之间参数透传。同一界面中不同层级控件间透传。该系列的目标是消除这两种参数透传使得不同界面以及同一界面内各层级间更加解耦降低参数传递开发的复杂度减少出错的可能增加可维护性。上一篇通过向前查询参数的方式解决了第一个 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 : BaseMvpFragmentEffectDetailsContract.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(): HashMapString, String? }为 View 新增一个扩展属性让每个控件都持有一个 TrackNodevar 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(): HashMapString, 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(): HashMapString, String? { return null } }这样一来一个窗口中整个 View 树的任何一个结点都具备了携带参数的能力从最顶层的 Activity到其内部的 Fragment再到任何一个控件。参数就不再需要自顶向下透传而是可以自底向上查询fun View.getTrackNode(): HashMapString, String { val map hashMapOfString, 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(): HashMapString, 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() } }消费参数时不再通过上一个界面透传而是通过自底向上的查询。因为约定的参数是 HashMapString, 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 变换为 类型 Rlet 方法内部只是通过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 设置为 trueoverride 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 mutableSetOfInt() 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 APP4、Flutter开发环境搭建和调试5、Dart语法篇之基础语法(一)6、Dart语法篇之集合的使用与源码解析(二)7、Dart语法篇之集合操作符函数与源码分析(三)…八、微信小程序开发1、小程序概述及入门2、小程序UI开发3、API操作4、购物商场项目实战……全套视频资料一、面试合集二、源码解析合集三、开源框架合集欢迎大家一键三连支持若需要文中资料直接点击文末CSDN官方认证微信卡片免费领取↓↓↓
返回列表