DEV Community

AblerSong
AblerSong

Posted on

MVM for h5,iOS,Android;Page Object Model, also known as POM

MVMDemo

h5,iOS,Android, software architecture, MVM (Mediator, View, ViewModel)

The design was originally conceptualized following the principles of MVM (Mediator, View, ViewModel). Upon completion, I conducted an online search to identify similar approaches. Eventually, Page Object Model, also known as POM, is a design pattern in Selenium that creates an object repository for storing all web elements. However, In iOS, Android, and H5, I have not yet found design pattern related to this aspect.

I. Introduction to MVM

At its core, it functions as an intermediary between the backend and the UI. It sends requests to the backend for interface data, parses the data, and produces a data model suitable for the UI's requirements, encompassing both data and function components. The frontend then directly renders this model, freeing UI developers from the need to address the underlying logic. Simultaneously, logic developers are liberated from focusing on UI concerns.

start in demo, simple code, beginner developers can easily understand
implementation:https://github.com/AblerSong/MVMDemo

II. Design Approach

  • Create a Page (iOS: ViewController; Android: Activity or fragment; Vue:.vue)
  • Divide the page UI into different components, with each component corresponding to a ViewModel. UI text is defined as a variable within the ViewModel, while UI click events are defined as closures.
  • Establish a Mediator that, upon completion of the API request, initializes all ViewModel variables and closures based on the data returned from the backend. Refer to IV. Code Implementation for specific details.
  • Provide the Mediator to UI developers, who can directly bind and render the UI based on the data contained within the Mediator.

III. Specific Implementation and Detailed Requirements (Reference Solution)

  • ##### Split each page into separate PageMediator instances, following the division of pages.
  • ##### Divide a page into different components based on "rows", where each component corresponds to a specific ViewModel, all managed through the PageMediator.
  • ##### Utilize a MediatorManager Singleton for each PageMediator (in the H5 demo, can use Vuex). This approach ensures data is keep alive, whereas UI elements are not keep alive.

MediatorManager Singleton manages PageMediators, and each PageMediator manages ViewModels as outlined below:

MediatorManager.getSingleton().mediator = new Mediator
MediatorManager.getSingleton().mediator = null
Enter fullscreen mode Exit fullscreen mode
  • If a page contains numerous components, the PageMediator could become complex. In such scenarios, you could utilize appropriate design patterns to refactor the PageMediator. The specific approach to splitting depends on individual preferences and architectural capabilities.
  • It's crucial to carefully consider public variables and methods within PageMediator. As long as the exposed API for UI is sound, subsequent logic refactoring won't impact the UI, and altering the UI will have minimal effect on the logic.
  • In practical development, other modules should also be encapsulated to facilitate future use of Unit Test as a replacement for UI Test. For instance, modules like Router, Toast, and Network. For example, for Toast:
class ToastViewModel {
    // use ReactiveX replace setter
    set toast(value) {
        Toast(value)
    }
}
Enter fullscreen mode Exit fullscreen mode

IV. Code Implementation

Image description

The following code demonstrates that Vue uses bind, iOS uses tableView, and Android uses Adapter for rendering. Data binding is performed within each page's component, while all the logic is centralized in the Mediator.

VUE

Mediator {
  username_text = "username"
  password_text = "password"
  _username_str = ""
  _password_str = ""
  login_btn_disabled = true

  constructor() {
    this.init()
  }
  init() {}

  set username_str(value) {
    this._username_str = value
    this.update_login_btn_disabled()
  }
  get username_str() {
    return this._username_str
  }

  set password_str(value) {
    this._password_str = value
    this.update_login_btn_disabled()
  }
  get password_str() {
    return this._password_str
  }

  update_login_btn_disabled() {
    this.login_btn_disabled = !(this.username_str?.length && this.password_str?.length)
  }

  onSubmit() {
    if (this.username_str == "admin" && this.password_str == "123456") {
      router.back()
    } else {
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Android

class ButtonViewModel (
    val buttonState: BehaviorSubject<Boolean> = BehaviorSubject.createDefault(false),
    var buttonText: BehaviorSubject<String> = BehaviorSubject.createDefault(""),
) {
    var clickItem = {}
}

class InputViewModel (
    val text: BehaviorSubject<String> = BehaviorSubject.createDefault(""),
    val value: BehaviorSubject<String> = BehaviorSubject.createDefault("")
) {}

class Mediator : BaseMediator () {
    val usernameViewModel: InputViewModel = InputViewModel()
    val passwordViewModel: InputViewModel = InputViewModel()
    val buttonViewModel: ButtonViewModel = ButtonViewModel()

    init {
        usernameViewModel.text.onNext("username")
        passwordViewModel.text.onNext("password")

        val isNotEmpty: (String, String) -> Boolean = { name: String, age: String ->
            name.isNotEmpty() && age.isNotEmpty()
        }
        val d1 = Observable.combineLatest(usernameViewModel.value, passwordViewModel.value, isNotEmpty).subscribe {
            buttonViewModel.buttonState.onNext(it)
        }

        buttonViewModel.clickItem = {
            val username = usernameViewModel.value.value
            val password = passwordViewModel.value.value
            if (username == "admin" && password == "123456") {
                routerSubject.onNext(R.layout.activity_main)
            } else {
                ToastManager.toastSubject.onNext("input error")
            }
        }

        compositeDisposable.add(d1)
    }

    val dataList by lazy { initList() }

    private fun initList(): List<Map<String, Any>> {
        val m1 = mapOf(Pair("viewType", R.layout.input_item), Pair("viewHolder", InputViewHolder::class.java), Pair("viewModel", usernameViewModel))
        val m2 = mapOf(Pair("viewType", R.layout.input_item), Pair("viewHolder", InputViewHolder::class.java), Pair("viewModel", passwordViewModel))
        val m3 = mapOf(Pair("viewType", R.layout.button_item), Pair("viewHolder", ButtonViewHolder::class.java), Pair("viewModel", buttonViewModel))

        return listOf<Map<String, Any>>(m1, m2, m3)
    }
}
Enter fullscreen mode Exit fullscreen mode

iOS

class ButtonCellViewModel: BaseViewModel {
    let login_btn_disabled = BehaviorRelay(value: false)
    var onSubmit = {}
}

class TextFieldCellViewModel: BaseViewModel {
    let text = BehaviorRelay(value: "")
    let value = BehaviorRelay(value: "")
}

class Mediator: BaseMediator {
    let usernameViewModel = TextFieldCellViewModel()
    let passwordViewModel = TextFieldCellViewModel()
    let buttonCellViewModel = ButtonCellViewModel()

    lazy var list: [[[String : Any]]] = {
        let arr: [[[String : Any]]] = [
            [
                ["model":usernameViewModel,"reuseIdentifier":textFieldCellReuseIdentifier],
                ["model":passwordViewModel,"reuseIdentifier":textFieldCellReuseIdentifier],
            ],
            [
                ["model":buttonCellViewModel,"reuseIdentifier":buttonCellReuseIdentifier]
            ]
        ]
        return arr
    }()

    override init() {
        super.init()

        initPasswordViewModel()
        initUsernameViewModel()
        initButtonCellViewModel()
    }

    func initUsernameViewModel() {
        usernameViewModel.text.accept("username")
    }
    func initPasswordViewModel() {
        passwordViewModel.text.accept("password")
    }
    func initButtonCellViewModel() {
        let combineLatest = Observable.combineLatest(usernameViewModel.value, passwordViewModel.value)

        combineLatest.map { (username: String, password: String) -> Bool in
            return username.count > 0 && password.count > 0
        }.bind(to: buttonCellViewModel.login_btn_disabled).disposed(by: disposeBag)


        buttonCellViewModel.onSubmit = {
            combineLatest.subscribe( onNext: { (username: String, password: String) in
                if username == "Admin", password == "123456" {
                    RouterBehaviorSubject.onNext(RouterModel(type: .pop))
                } else {
                    ToastBehaviorSubject.onNext("input error")
                }
            }).dispose()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

V. Advantages and Disadvantages

Advantages:

  • Compared to frameworks like VIPER, MVI, etc., the core idea is simpler and easier to grasp.
  • Separation of UI and logic facilitates task decomposition and combination, enhancing code reusability.
  • Properly decomposed, the Mediator can be unit test in place of UI test; this facilitates straightforward white-box automated testing.
  • Thanks to the presence of the MediatorManager Singleton, data remains alive, while UI doesn't; data is centralized, enabling easy management.
  • Uniform business code structure allows developers to quickly take over others' code.

Disadvantages:

  • Developers not paying attention can easily cause memory leaks, which may be challenging to locate.

VI. Conclusion

From a practical development perspective, this framework is exceptionally well-suited for H5 applications. For example, in the demo (Vue), breaking down components into .vue, .scss, and .js files greatly enhances code reusability, particularly due to the independent nature of CSS files.

For H5, iOS, and Android, utilizing the Mediator for Unit test in place of UI test can significantly reduce errors and enhance testing efficiency.

Personally, I am highly impressed with this approach. It substantially boosts the efficiency of automated testing, making UI test much less cumbersome compared to the convenience of Unit test.

Top comments (0)