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

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

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

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

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

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

二、解决思路

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

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

下面都会给出答案。

三、具体实践

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

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

先看看模块一中的代码:

kotlin 复制代码
/**
 * 模块一具有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的生命周期感知能力,用于在页面销毁时取消和销毁计时。具备页面生命周期感知能力是模块很重要的特性。

再看看模块二中的代码:

kotlin 复制代码
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有全量刷新和局部刷新的方法,对于刷新也比较友好。

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

kotlin 复制代码
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)
        )
    }

}
kotlin 复制代码
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,代码如下:

kotlin 复制代码
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

kotlin 复制代码
object ModuleConstant {
    //ItemType
    const val MODULE_ONE_ITEM_TYPE = 0
    const val MODULE_TWO_ITEM_TYPE = 1
}

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

kotlin 复制代码
data class ModuleOneData(
    var text: String? = ""
)

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

kotlin 复制代码
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"
    }
    
}
kotlin 复制代码
interface ModuleTwoItemBinderInterface {

    //开始计时
   fun onStartTimer()

    //停止计时
    fun onStopTimer()

    //获取计时数据
    fun onGetTimerNumber():String
}

四、总结

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

五、注意事项

1、不要把太复杂的UI交互放在单一模块,处理起来费劲。

2、如果二个模块中间需要大量的通讯,写太多接口也费劲,最好看能不能放一个模块。

3、数据最好请求好后再塞进去给各个ItemBinder用,方便统一处理UI。当然如果各个模块想自己处理UI,那各个模块也可以自己去请求接口。毕竟模块隔离,彼此也互不影响。

4、页面如果不是很复杂,不需要拆分成模块,不需要使用这种方式,直接一个XML搞定,清晰简单。

时间仓促,如有错误欢迎批评指正!!

相关推荐
C4rpeDime34 分钟前
自建MD5解密平台-续
android
鲤籽鲲2 小时前
C# Random 随机数 全面解析
android·java·c#
m0_548514776 小时前
2024.12.10——攻防世界Web_php_include
android·前端·php
凤邪摩羯6 小时前
Android-性能优化-03-启动优化-启动耗时
android
凤邪摩羯6 小时前
Android-性能优化-02-内存优化-LeakCanary原理解析
android
喀什酱豆腐7 小时前
Handle
android
m0_748232928 小时前
Android Https和WebView
android·网络协议·https
m0_748251729 小时前
Android webview 打开本地H5项目(Cocos游戏以及Unity游戏)
android·游戏·unity
m0_7482546611 小时前
go官方日志库带色彩格式化
android·开发语言·golang
zhangphil11 小时前
Android使用PorterDuffXfermode模式PorterDuff.Mode.SRC_OUT橡皮擦实现“刮刮乐”效果,Kotlin(2)
android·kotlin