Android 复杂UI界面分模块解耦的一次实践

news2024/11/25 13:51:22

一、复杂UI页面开发的问题

常见的比较复杂的UI界面,比如电商首页,我们看看某电商的首页部分UI:

上面是截取的首页部分,如果这个首页如果不分模块开发会遇到哪些问题?

  • 开发任务不方便分割,一个人开发的话周期会很长
  • 在XML文件中写死首页布局不够灵活
  • 逻辑和UI塞在一起不方便维护
  • 首页不能动态化配置
  • UI和逻辑难以复用

那如何解决这个问题? 下面是基于基于 BRVAH 3.0.11版本 实现的复杂页面分模块的UI和逻辑的解耦。

二、解决思路

使用RecyclerView在BRVAH中利用不同的ViewType灵活的组装页面。但也面临一些问题,比如:

  • 如何实现模块间的通讯和互传数据?
  • 如何实现模块整理刷新和局部刷新?

下面都会给出答案。

三、具体实践

我们先看看模块拆分组装UI实现的效果:

模块二中有三个按钮,前面两个按钮可以启动和停止模块一中的计数,最后一个按钮获取模块一中的计数值。对应的就是模块间通讯和获取数据。

先看看模块一中的代码:

/**
 * 模块一具有Activity生命周期感知能力
 */
class ModuleOneItemBinder(
 private val lifecycleOwner: LifecycleOwner
) : QuickViewBindingItemBinder<ModuleOneData, LayoutModuleOneBinding>(),
 LifecycleEventObserver, MultiItemEntity {
 private var mTimer: Timer? = null
 private var mIsStart: Boolean = true    //是否开始计时
 private var number: Int = 0
 private lateinit var mViewBinding: LayoutModuleOneBinding
 init {
 lifecycleOwner.lifecycle.addObserver(this)
 }
 @SuppressLint("SetTextI18n")
 override fun convert(
 holder: BinderVBHolder<LayoutModuleOneBinding>,
 data: ModuleOneData
 ) {
 //TODO 根据数据设置模块的UI
 }
 override fun onCreateViewBinding(
 layoutInflater: LayoutInflater,
 parent: ViewGroup,
 viewType: Int
 ): LayoutModuleOneBinding {
 mViewBinding = LayoutModuleOneBinding.inflate(layoutInflater, parent, false)
 return mViewBinding
 }
 /**
 * 向外暴露调用方法
 * 开始计时
 */
 fun startTimer() {
 if (mTimer != null) {
 mIsStart = true
 } else {
 mTimer = fixedRateTimer(period = 1000L) {
 if (mIsStart) {
 number++
 //修改Adapter中的值,其他模块可以通过Adapter取到这个值,也可以通过接口抛出去,这里是提供另一种思路。
 (data[0] as ModuleOneData).text = number.toString()
 mViewBinding.tv.text = "计时:$number"
 }
 }
 }
 }
 /**
 * 向外暴露调用方法
 * 停止计时
 */
 fun stopTimer() {
 mTimer?.apply {
 mIsStart = false
 }
 }
 /**
 * 生命周期部分的处理
 */
 override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
 when (event) {
 Lifecycle.Event.ON_DESTROY -> {
 //页面销毁时计时器也取消和销毁
 lifecycleOwner.lifecycle.removeObserver(this)
 mTimer?.cancel()
 mTimer = null
 }
 else -> {}
 }
 }
 /**
 * 设定itemType
 */
 override val itemType: Int
 get() = MODULE_ONE_ITEM_TYPE
}

模块一向外暴露了startTimer()stopTimer()二个方法,并且让模块一具备了Activity的生命周期感知能力,用于在页面销毁时取消和销毁计时。具备页面生命周期感知能力是模块很重要的特性。

再看看模块二中的代码:

class ModuleTwoItemBinder(private val moduleTwoItemBinderInterface: ModuleTwoItemBinderInterface) :
 QuickViewBindingItemBinder<ModuleTwoData, LayoutModuleTwoBinding>(), MultiItemEntity {
 @SuppressLint("SetTextI18n")
 override fun convert(
 holder: BinderVBHolder<LayoutModuleTwoBinding>,
 data: ModuleTwoData
 ) {
 holder.viewBinding.btStartTimer.setOnClickListener {  //接口实现
 moduleTwoItemBinderInterface.onStartTimer()
 }
 holder.viewBinding.btStopTimer.setOnClickListener {  //接口实现
 moduleTwoItemBinderInterface.onStopTimer()
 }
 holder.viewBinding.btGetTimerNumber.setOnClickListener {  //接口实现
 holder.viewBinding.tv.text =
 "获取到的模块一的计时数据:" + moduleTwoItemBinderInterface.onGetTimerNumber()
 }
 }
 /**
 * 可以做局部刷新
 */
 override fun convert(
 holder: BinderVBHolder<LayoutModuleTwoBinding>,
 data: ModuleTwoData,
 payloads: List<Any>
 ) {
 super.convert(holder, data, payloads)
 if (payloads.isNullOrEmpty()) {
 convert(holder, data)
 } else {
 //TODO 根据具体的payloads做局部刷新
 }
 }
 override fun onCreateViewBinding(
 layoutInflater: LayoutInflater,
 parent: ViewGroup,
 viewType: Int
 ): LayoutModuleTwoBinding {
 return LayoutModuleTwoBinding.inflate(layoutInflater, parent, false)
 }
 override val itemType: Int
 get() = MODULE_TWO_ITEM_TYPE
}

模块二中有一个ModuleTwoItemBinderInterface接口对象,用于调用接口方法,具体接口实现在外部。convert有全量刷新和局部刷新的方法,对于刷新也比较友好。

接着看看是如何把不同的模块拼接起来的:

class MultipleModuleTestAdapter(
 private val lifecycleOwner: LifecycleOwner,
 data: MutableList<Any>? = null
) : BaseBinderAdapter(data) {
 override fun getItemViewType(position: Int): Int {
 return position + 1
 }
 /**
 * 给类型一和类型二设置数据
 */
 fun setData(response: String) {
 val moduleOneData = ModuleOneData().apply { text = "模块一数据:$response" }
 val moduleTwoData = ModuleTwoData().apply { text = "模块二数据:$response" }
 //给Adapter设置数据
 setList(arrayListOf(moduleOneData, moduleTwoData))
 }
 /**
 * 添加ItemType类型一
 */
 fun addItemOneBinder() {
 addItemBinder(
 ModuleOneData::class.java,
 ModuleOneItemBinder(lifecycleOwner)
 )
 }
 /**
 * 添加ItemType类型二
 */
 fun addItemTwoBinder(moduleTwoItemBinderInterface: ModuleTwoItemBinderInterface) {
 addItemBinder(
 ModuleTwoData::class.java,
 ModuleTwoItemBinder(moduleTwoItemBinderInterface)
 )
 }
}` 

class MainModuleManager(
 private val activity: MainActivity,
 private val viewModel: MainViewModel,
 private val viewBinding: ActivityMainBinding
) {
 private var multipleModuleTestAdapter: MultipleModuleTestAdapter? = null
 /**
 * 监听请求数据的回调
 */
 fun observeData() {
 viewModel.requestDataLiveData.observe(activity) {
 //接口请求到的数据
 initAdapter(it)
 }
 }
 private fun initAdapter(response: String) {
 //创建Adapter
 multipleModuleTestAdapter = MultipleModuleTestAdapter(activity)
 //设置RecyclerView
 viewBinding.rcy.apply {
 layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
 adapter = multipleModuleTestAdapter
 }
 //创建ModuleTwoItemBinder的接口实现类
 val moduleTwoItemBinderImpl = ModuleTwoItemBinderImpl(multipleModuleTestAdapter)
 //添加Item类型,组装UI,可以根据后台数据动态化
 multipleModuleTestAdapter?.addItemOneBinder()
 multipleModuleTestAdapter?.addItemTwoBinder(moduleTwoItemBinderImpl)
 //给所有的Item添加数据
 multipleModuleTestAdapter?.setData(response)
 }
 /**
 * 刷新单个模块的数据,也可以刷新单个模块的某个部分,需要设置playload
 */
 fun refreshModuleData(position: Int, newData: Any?) {
 multipleModuleTestAdapter?.apply {
 newData?.let {
 data[position] = newData
 notifyItemChanged(position)
 }
 }
 }
}

MultipleModuleTestAdapter中定义了多种ViewType,通过MainModuleManager返回的数据,动态的组装添加ViewType

最后就是在MainActivity中调用MainModuleManager,代码如下:

class MainActivity : AppCompatActivity() {
 private val mainViewModel: MainViewModel by viewModels()
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 val activityMainBinding: ActivityMainBinding =
 ActivityMainBinding.inflate(layoutInflater)
 setContentView(activityMainBinding.root)
 //请求数据
 mainViewModel.requestData()
 //拆分RecyclerView的逻辑
 val mainModuleManager = MainModuleManager(this, mainViewModel, activityMainBinding)
 //回调数据到MainModuleManager中
 mainModuleManager.observeData()
 //TODO 如果有其他控件编写其他控件的逻辑
 }
  
}

这样我们通过定义不同的ItemBinder实现了模块的划分,通过定义接口实现了模块间的通讯,通过后台返回数据动态的组装了页面。

其他代码一并写在末尾,方便阅读和理解:

image.png

ModuleConstant

`object ModuleConstant {
 //ItemType
 const val MODULE_ONE_ITEM_TYPE = 0
 const val MODULE_TWO_ITEM_TYPE = 1
}` 

ModuleOneDataModuleTwoData都是data类,内容完全一致,随便定义的:

`data class ModuleOneData(
 var text: String? = ""
)

ModuleTwoItemBinderImplModuleTwoItemBinderInterface的实现类,通过Adapter能轻松的获取到不同的ItemBinder,所以可以通过接口互相调用彼此的函数。

class ModuleTwoItemBinderImpl(private val multipleModuleTestAdapter: MultipleModuleTestAdapter?) :
 ModuleTwoItemBinderInterface {
 /**
 * 外部实现里面的方法
 */
 override fun onStartTimer() {
 //通过`Adapter`能轻松的获取到不同的`ItemBinder`,所以可以通过接口互相调用彼此的函数
 val moduleOneItemBinder =
 multipleModuleTestAdapter?.getItemBinder(ModuleConstant.MODULE_ONE_ITEM_TYPE + 1) as ModuleOneItemBinder
 moduleOneItemBinder.startTimer()
 }
 override fun onStopTimer() {
 //通过`Adapter`能轻松的获取到不同的`ItemBinder`,所以可以通过接口互相调用彼此的函数
 val moduleOneItemBinder =
 multipleModuleTestAdapter?.getItemBinder(ModuleConstant.MODULE_ONE_ITEM_TYPE + 1) as ModuleOneItemBinder
 moduleOneItemBinder.stopTimer()
 }
 override fun onGetTimerNumber(): String {
 multipleModuleTestAdapter?.apply {
 //通过Adapter可以轻松的拿到其他模块的数据
 return (data[0] as ModuleOneData).text ?: "0"
 }
 return "0"
 }
  
} 

nterface ModuleTwoItemBinderInterface {
 //开始计时
 fun onStartTimer()
 //停止计时
 fun onStopTimer()
 //获取计时数据
 fun onGetTimerNumber():String
}

四、总结

通过定义不同的ItemBinder将页面划分为不同模块,实现UI和交互解耦,单个ItemBinder也可以在其他页面进行复用。通过后台数据动态的添加ItemBinder页面组装更灵活。任务分拆,提高开发效率。

五、注意事项

1、不要把太复杂的UI交互放在单一模块,处理起来费劲。
2、如果二个模块中间需要大量的通讯,写太多接口也费劲,最好看能不能放一个模块。
3、数据最好请求好后再塞进去给各个ItemBinder用,方便统一处理UI。当然如果各个模块想自己处理UI,那各个模块也可以自己去请求接口。毕竟模块隔离,彼此也互不影响。
4、页面如果不是很复杂,不需要拆分成模块,不需要使用这种方式,直接一个XML搞定,清晰简单。

Android 学习笔录

Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1167583.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

毅速丨3D打印结合拓扑优化让轻量化制造更容易

轻量化可以减少产品的重量&#xff0c;提高产品的性能和效率&#xff0c;同时减少能源消耗和排放。尤其在航空航天、汽车制造造等行业对轻量化追求更高。当前&#xff0c;随着制造技术的发展&#xff0c;拓扑优化结合3D打印为轻量化制造带来的显著的优势正在逐渐凸显。 首先&am…

寻找更好的发展机会

一个员工离开一家公司的原因无外乎下面几个原因&#xff1a; 第一&#xff0c;对薪资不满意。 第二&#xff0c;在公司没前途。 第三&#xff0c;工作干的不爽。 从题主的叙述来看&#xff0c;上面三条都被题主占全了。 第一条&#xff0c;题主从日本母公司回国后&#xf…

数据库概述 -- 数据模型知识点要点详解

数据模型 概述概念数据特征组成要素分类概念层数据模型逻辑层数据模型物理层数据模型 主页传送门&#xff1a;&#x1f4c0; 传送 概述 数据模型是对现实世界数据特征的抽象&#xff0c;它描述了数据的结构、操作和约束条件&#xff0c;为数据库系统的信息表示与操作提供一个抽…

【文生图】Stable Diffusion XL 1.0模型Full Fine-tuning指南(U-Net全参微调)

文章目录 前言重要教程链接以海报生成微调为例总体流程数据获取POSTER-TEXTAutoPosterCGL-DatasetPKU PosterLayoutPosterT80KMovie & TV Series & Anime Posters 数据清洗与标注模型训练模型评估生成图片样例宠物包商品海报护肤精华商品海报 一些TipsMata&#xff1a;…

第6章_多表查询

文章目录 多表查询概述1 一个案例引发的多表连接1.1 案例说明1.2 笛卡尔积理解演示代码 2 多表查询分类讲解2.1 等值连接 & 非等值连接2.1.1 等值连接2.1.2 非等值连接 自连接 & 非自连接内连接与外连接演示代码 3 SQL99语法实现多表查询3.1 基本语法3.2 内连接&#x…

kubernetes集群编排——service微服务

service微服务 创建测试示例 vim myapp.yml apiVersion: apps/v1kind: Deploymentmetadata:labels:app: myappname: myappspec:replicas: 6selector:matchLabels:app: myapptemplate:metadata:labels:app: myappspec:containers:- image: myapp:v1name: myapp---apiVersion: v1…

趋势:实时的stable diffusion

视频中使用了实时模型&#xff1a;只需2~4 个步骤甚至一步即可生成768 x 768分辨率图像。 这项技术可以把任意的stable diffusion模型转为实时模型。 潜在一致性模型 LCM LCM 只需 4,000 个训练步骤&#xff08;约 32 个 A100 GPU 一小时&#xff09;即可从任何预训练的SD模型中…

每个程序员都应该知道的六种负载均衡算法

一个大型网络平台能轻松面对数百万请求而不产生崩溃&#xff0c;负载均衡器&#xff08;Load Balancer&#xff09;是绝对的关键组件。 负载均衡器会在多个服务器之间分配工作流&#xff0c;也就是将用户请求转发到不同的机器上&#xff0c;可以确保服务的高可用性、响应速度和…

揭秘重生奇迹mu中的幻术园

幻术园可以说是重生奇迹mu游戏进化中的一个里程碑&#xff0c;因为一个重要的地图区域就此开发出来&#xff0c;同时它还是继勇者大陆、仙踪林后的第三个新人出生地&#xff0c;所以这种象征性的意义更为重要&#xff0c;今天小编就带领大家进入到重生奇迹mu发布网下的幻术园中…

10+ Web3 新锐联合放送知识与奖励,一起瓜分 2000 美金!

Web3 时代&#xff0c;链接更多的参与者&#xff0c;激发共创活跃度&#xff0c;是一个成熟繁荣的项目和社区生态所必备的要素。深度协作与互动&#xff0c;始终是打破无形壁垒&#xff0c;构建高度融合和包容的社区生态的重要路径&#xff0c;也是培养用户和开发者参与建设的热…

浅谈低压无功补偿在分布式光伏现场中的应用-安科瑞 蒋静

摘要&#xff1a;分布式光伏电站由于建设时间短、技术成熟、收益明显而发展迅速&#xff0c;但光伏并网引起用户功率因数异常的问题也逐渐凸显。针对分布式光伏电站接入配电网后功率因数降低的问题&#xff0c;本文分析了低压无功补偿装置补偿失效的原因&#xff0c;并提出了一…

Seata入门系列【19】分布式事务之CAP、BASE理论

1 CAP理论 CAP是以下三个词语的缩写&#xff1a; Consistency&#xff1a;一致性Availability&#xff1a;可用性Partition tolerance&#xff1a;分区容忍性 CAP理论的基础概念就是在分布式系统中&#xff0c;无法同时满足以上三点。 下面我们以一个简单的分布式系统&…

如何减少自动化测试的误差?

自动化测试是一种利用软件工具或者硬件设备来代替人工执行测试用例的方法&#xff0c;它可以提高测试效率和质量&#xff0c;但也可能存在一些误差&#xff0c;影响测试结果的准确性和可信度。 造成自动化测试结果有误差的原因主要有以下两类&#xff1a; 系统误差&#xff1a…

服务器黑洞,如何秒解

想必这样的短信大家都应该见过吧&#xff0c;这其实是阿里云服务器被攻击后触发的黑洞机制的短信通知。还有很多朋友不知道&#xff0c;为什么要这么做。原因其实很简单啊&#xff0c;当同一个机房的ip段&#xff0c;如果说有一台服务器遭受低道攻击&#xff0c;那么很可能会造…

手机知识:手机“飞行模式”你真的会用吗,看完你就懂了

目录 “飞行模式”的实用技能 关于手机的谣言 回想一下&#xff0c;当你第一次知道手机上的“飞行模式”时&#xff0c;你认为这是一个怎样的功能&#xff1f; 普通青年&#xff1a;在飞机上要使用的模式。 文艺青年&#xff1a;手机终日忙忙碌碌&#xff0c;偶尔也需要放飞…

香港高端人才通行证计划申请攻略:条件+材料清单+流程!

香港高端人才通行证计划申请攻略&#xff1a;条件材料清单流程&#xff01; 香港高才通计划希望吸引世界各地具备丰富工作经验及高学历的人才到香港探索机遇&#xff0c;这些高端人才包括高收入人士和在世界顶尖大学毕业的学生。 此计划并不适用于阿富汗、古巴、老挝、朝鲜、尼…

视觉问答(VQA)12篇顶会精选论文合集,附常用数据集下载

今天来聊聊计算机视觉和自然语言处理交叉的一个热门研究方向&#xff1a;视觉问答&#xff08;VQA&#xff09;。 视觉问答的任务是&#xff1a;给出一张图片和一个关于这张图片的自然语言问题&#xff0c;计算机需要根据图片的内容自动回答这个问题。这样的任务考验了计算机在…

ucgui 画圆弧时圆弧有缺口

一、问题描述 使用抗锯齿的方式画圆弧&#xff0c;在画到90的位置时&#xff0c;那个位置刚好没画上&#xff0c;留了个缺口&#xff0c;其他位置一切绘制正常。 使用非抗锯齿的方式画圆弧&#xff0c;没出现缺口。 二、问题原因 当初在移植ucgui 时&#xff0c;底层函数 …

【2021研电赛】基于图像处理的物体识别与分类系统

本作品介绍参与极术社区的有奖征集|分享研电赛作品扩大影响力&#xff0c;更有重磅电子产品免费领取! 团队介绍 学校名称&#xff1a;北京理工大学 队伍名称&#xff1a;BIT铁头帮 指导教师&#xff1a;冯云鹏 队伍成员&#xff1a;余佳桐 孙雨婷 朱翊铭 获奖情况&#xff1a…

C++之栈容器

1.简介 stack &#xff0c;栈(堆栈)&#xff0c;是一种先进后出(First In Last Out,FILO)的数据结构&#xff0c;先插入的数据在栈底&#xff0c;后放入的数据在栈顶&#xff0c;所有的数据只能从栈顶取出。   在生活中先进后出的例子友很多&#xff0c;例如我们在桌子上摞书…