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搞定,清晰简单。

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

相关推荐
Kapaseker1 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴2 小时前
Android17 为什么重写 MessageQueue
android
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android