A practice of decoupling Android complex UI interface into modules

1. Problems with complex UI page development

Common and complex UI interfaces, such as e-commerce homepages, let’s take a look at part of the UI on the homepage of an e-commerce company:

The above is an intercepted part of the homepage. What problems will be encountered if this homepage is developed without dividing it into modules?

  • Development tasks are inconvenient to divide. If one person develops it, the cycle will be very long.
  • Hard-coding the homepage layout in the XML file is not flexible enough
  • Logic and UI are stuffed together and are inconvenient to maintain.
  • The homepage cannot be configured dynamically
  • UI and logic are difficult to reuse

So how to solve this problem? The following is the decoupling of UI and logic based on complex page modules implemented based on BRVAH version 3.0.11.

2. Solution ideas

Use RecyclerView to flexibly assemble pages using different ViewTypes in BRVAH. But we also face some problems, such as:

  • How to realize communication and data transfer between modules?
  • How to implement module finishing refresh and partial refresh?

The answers will be given below.

3. Specific Practice

Let’s first look at the effect of module splitting and assembly UI implementation:

There are three buttons in module two. The first two buttons can start and stop the counting in module one, and the last button gets the count value in module one. The corresponding is communication between modules and obtaining data.

Let’s first look at the code in module one:

/**
 * Module 1 has Activity life cycle awareness capability
 */
class ModuleOneItemBinder(
 private val lifecycleOwner: LifecycleOwner
): QuickViewBindingItemBinder<ModuleOneData, LayoutModuleOneBinding>(),
 LifecycleEventObserver, MultiItemEntity {
 private var mTimer: Timer? = null
 private var mIsStart: Boolean = true //Whether to start timing
 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 Set the UI of the module based on data
 }
 override fun onCreateViewBinding(
 layoutInflater: LayoutInflater,
 parent: ViewGroup,
 viewType: Int
 ): LayoutModuleOneBinding {
 mViewBinding = LayoutModuleOneBinding.inflate(layoutInflater, parent, false)
 return mViewBinding
 }
 /**
 * Expose the calling method to the outside world
 * start the timer
 */
 fun startTimer() {
 if (mTimer != null) {
 mIsStart = true
 } else {
 mTimer = fixedRateTimer(period = 1000L) {
 if (mIsStart) {
 number++
 //Modify the value in the Adapter. Other modules can get this value through the Adapter, or throw it out through the interface. Here is another idea.
 (data[0] as ModuleOneData).text = number.toString()
 mViewBinding.tv.text = "Timing: $number"
 }
 }
 }
 }
 /**
 * Expose the calling method to the outside world
 * Stop timing
 */
 fun stopTimer() {
 mTimer?.apply {
 mIsStart = false
 }
 }
 /**
 * Processing of life cycle part
 */
 override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
 when (event) {
 Lifecycle.Event.ON_DESTROY -> {
 //The timer is also canceled and destroyed when the page is destroyed
 lifecycleOwner.lifecycle.removeObserver(this)
 mTimer?.cancel()
 mTimer = null
 }
 else -> {}
 }
 }
 /**
 * Set itemType
 */
 override val itemType: Int
 get() = MODULE_ONE_ITEM_TYPE
}

Module 1 exposes two methods: startTimer() and stopTimer(), and gives module 1 the ability to sense the life cycle of Activity, which is used to cancel when the page is destroyed. and destruction timing. The ability to be aware of the page life cycle is a very important feature of the module.

Take another look at the code in module two:

class ModuleTwoItemBinder(private val moduleTwoItemBinderInterface: ModuleTwoItemBinderInterface):
 QuickViewBindingItemBinder<ModuleTwoData, LayoutModuleTwoBinding>(), MultiItemEntity {
 @SuppressLint("SetTextI18n")
 override fun convert(
 holder: BinderVBHolder<LayoutModuleTwoBinding>,
 data: ModuleTwoData
 ) {
 holder.viewBinding.btStartTimer.setOnClickListener { //Interface implementation
 moduleTwoItemBinderInterface.onStartTimer()
 }
 holder.viewBinding.btStopTimer.setOnClickListener { //Interface implementation
 moduleTwoItemBinderInterface.onStopTimer()
 }
 holder.viewBinding.btGetTimerNumber.setOnClickListener { //Interface implementation
 holder.viewBinding.tv.text =
 "The timing data of module one obtained:" + moduleTwoItemBinderInterface.onGetTimerNumber()
 }
 }
 /**
 * Can do partial refresh
 */
 override fun convert(
 holder: BinderVBHolder<LayoutModuleTwoBinding>,
 data: ModuleTwoData,
 payloads: List<Any>
 ) {
 super.convert(holder, data, payloads)
 if (payloads.isNullOrEmpty()) {
 convert(holder, data)
 } else {
 //TODO partially refreshes based on specific 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
}

There is a ModuleTwoItemBinderInterface interface object in module two, which is used to call interface methods. The specific interface is implemented externally. convert has full refresh and partial refresh methods, which are also friendly to refresh.

Then take a look at how different modules are spliced together:

class MultipleModuleTestAdapter(
 private val lifecycleOwner: LifecycleOwner,
 data: MutableList<Any>? = null
) : BaseBinderAdapter(data) {
 override fun getItemViewType(position: Int): Int {
 return position + 1
 }
 /**
 * Set data for type one and type two
 */
 fun setData(response: String) {
 val moduleOneData = ModuleOneData().apply { text = "Module One Data:$response" }
 val moduleTwoData = ModuleTwoData().apply { text = "Module Two Data:$response" }
 //Set data for Adapter
 setList(arrayListOf(moduleOneData, moduleTwoData))
 }
 /**
 * Add ItemType type one
 */
 fun addItemOneBinder() {
 addItemBinder(
 ModuleOneData::class.java,
 ModuleOneItemBinder(lifecycleOwner)
 )
 }
 /**
 * Add ItemType type two
 */
 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
 /**
 * Callback for listening to request data
 */
 fun observeData() {
 viewModel.requestDataLiveData.observe(activity) {
 //Data requested by the interface
 initAdapter(it)
 }
 }
 private fun initAdapter(response: String) {
 //Create Adapter
 multipleModuleTestAdapter = MultipleModuleTestAdapter(activity)
 //Set up RecyclerView
 viewBinding.rcy.apply {
 layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
 adapter = multipleModuleTestAdapter
 }
 //Create the interface implementation class of ModuleTwoItemBinder
 val moduleTwoItemBinderImpl = ModuleTwoItemBinderImpl(multipleModuleTestAdapter)
 //Add Item type and assemble UI, which can be dynamic based on background data
 multipleModuleTestAdapter?.addItemOneBinder()
 multipleModuleTestAdapter?.addItemTwoBinder(moduleTwoItemBinderImpl)
 //Add data to all Items
 multipleModuleTestAdapter?.setData(response)
 }
 /**
 * Refresh the data of a single module or a certain part of a single module. You need to set the playload
 */
 fun refreshModuleData(position: Int, newData: Any?) {
 multipleModuleTestAdapter?.apply {
 newData?.let {
 data[position] = newData
 notifyItemChanged(position)
 }
 }
 }
}

Multiple ViewType are defined in MultipleModuleTestAdapter, and ViewType is dynamically assembled and added through the data returned by MainModuleManager.

The last step is to call MainModuleManager in MainActivity. The code is as follows:

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)
 //Request data
 mainViewModel.requestData()
 //Split the logic of RecyclerView
 val mainModuleManager = MainModuleManager(this, mainViewModel, activityMainBinding)
 //Call back data to MainModuleManager
 mainModuleManager.observeData()
 //TODO If there are other controls, write the logic of other controls
 }
  
}

In this way, we realize the division of modules by defining different ItemBinder, realize the communication between modules by defining interfaces, and dynamically assemble the page by returning data in the background.

Other codes are written at the end for easy reading and understanding:

image.png

ModuleConstant

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

ModuleOneData and ModuleTwoData are both data classes with exactly the same content and can be defined casually:

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

ModuleTwoItemBinderImpl is the implementation class of ModuleTwoItemBinderInterface. Different ItemBinder can be easily obtained through Adapter, so you can use the interface Call each other’s functions.

class ModuleTwoItemBinderImpl(private val multipleModuleTestAdapter: MultipleModuleTestAdapter?) :
 ModuleTwoItemBinderInterface {
 /**
 * Methods in external implementation
 */
 override fun onStartTimer() {
 //You can easily obtain different `ItemBinder` through `Adapter`, so you can call each other's functions through the interface
 val moduleOneItemBinder =
 multipleModuleTestAdapter?.getItemBinder(ModuleConstant.MODULE_ONE_ITEM_TYPE + 1) as ModuleOneItemBinder
 moduleOneItemBinder.startTimer()
 }
 override fun onStopTimer() {
 //You can easily obtain different `ItemBinder` through `Adapter`, so you can call each other's functions through the interface
 val moduleOneItemBinder =
 multipleModuleTestAdapter?.getItemBinder(ModuleConstant.MODULE_ONE_ITEM_TYPE + 1) as ModuleOneItemBinder
 moduleOneItemBinder.stopTimer()
 }
 override fun onGetTimerNumber(): String {
 multipleModuleTestAdapter?.apply {
 //You can easily get data from other modules through Adapter
 return (data[0] as ModuleOneData).text ?: "0"
 }
 return "0"
 }
  
}
nterface ModuleTwoItemBinderInterface {
 //start the timer
 fun onStartTimer()
 //Stop timing
 fun onStopTimer()
 //Get timing data
 fun onGetTimerNumber():String
}

4. Summary

By defining different ItemBinder, the page is divided into different modules to achieve decoupling of UI and interaction. A single ItemBinder can also be reused on other pages. Dynamically adding ItemBinder through background data makes page assembly more flexible. Split tasks to improve development efficiency.

5. Precautions

1. Don’t put too complex UI interactions in a single module, as it will be difficult to handle.
2. If a lot of communication is required between the two modules, and it is difficult to write too many interfaces, it is best to see if one module can be placed.
3. It is best to request the data and then stuff it into each ItemBinder to facilitate unified processing of the UI. Of course, if each module wants to handle the UI by itself, then each module can also request the interface by itself. After all, the modules are isolated and do not affect each other.
4. If the page is not very complex, there is no need to split it into modules. There is no need to use this method. It can be done directly with XML, which is clear and simple.

Android learning notes

Jetpack family bucket chapter (including Compose): https://qr18.cn/A0gajp
Android performance optimization: https://qr18.cn/FVlo89
The underlying principles of Android Framework: https://qr18.cn/AQpN4J
Android car version: https://qr18.cn/F05ZCM
Android reverse security study notes: https://qr18.cn/CQ5TcL
Android audio and video: https://qr18.cn/Ei3VPD
OkHttp source code analysis notes: https://qr18.cn/Cw0pBD
Kotlin article: https://qr18.cn/CdjtAF
Gradle article: https://qr18.cn/DzrmMB
Flutter article: https://qr18.cn/DIvKma
Eight major bodies of Android knowledge: https://qr18.cn/CyxarU
Android core notes: https://qr21.cn/CaZQLo
Android interview questions from previous years: https://qr18.cn/CKV8OZ
The latest Android interview question set in 2023: https://qr18.cn/CgxrRy
Interview questions for Android vehicle development positions: https://qr18.cn/FTlyCJ
Audio and video interview questions: https://qr18.cn/AcV6Ap