Android 复杂UI界面分模块解耦的一次实践
一、复杂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: ViewGr0up,
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: ViewGr0up,
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
实现了模块的划分,通过定义接口实现了模块间的通讯,通过后台返回数据动态的组装了页面。
其他代码一并写在末尾,方便阅读和理解:
ModuleConstant
object ModuleConstant {
//ItemType
const val MODULE_ONE_ITEM_TYPE = 0
const val MODULE_TWO_ITEM_TYPE = 1
}
ModuleOneData
和ModuleTwoData
都是data
类,内容完全一致,随便定义的:
data class ModuleOneData(
var text: String? = ""
)
ModuleTwoItemBinderImpl
是ModuleTwoItemBinderInterface
的实现类,通过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"
}
}
interface ModuleTwoItemBinderInterface {
//开始计时
fun onStartTimer()
//停止计时
fun onStopTimer()
//获取计时数据
fun onGetTimerNumber():String
}
四、总结
通过定义不同的ItemBinder
将页面划分为不同模块,实现UI和交互解耦,单个ItemBinder
也可以在其他页面进行复用。通过后台数据动态的添加ItemBinder
页面组装更灵活。任务分拆,提高开发效率。
五、注意事项
1、不要把太复杂的UI交互放在单一模块,处理起来费劲。
2、如果二个模块中间需要大量的通讯,写太多接口也费劲,最好看能不能放一个模块。
3、数据最好请求好后再塞进去给各个ItemBinder用,方便统一处理UI。当然如果各个模块想自己处理UI,那各个模块也可以自己去请求接口。毕竟模块隔离,彼此也互不影响。
4、页面如果不是很复杂,不需要拆分成模块,不需要使用这种方式,直接一个XML搞定,清晰简单。
时间仓促,如有错误欢迎批评指正!!
来源:juejin.cn/post/7296865632166477833