MVM for h5,iOS,Android;Page Object Model, also known as POM
Tags: javascript, ios, android, designpatterns
Published: 2023-08-12 03:06:43

# 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](https://github.com/AblerSong/MVMDemo), 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](#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
```

- ##### 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)
    }
}
```


### IV. Code Implementation


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4vtdtjuq538tff9fch7g.png)


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 {
    }
  }
}
```


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)
    }
}
```

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()
        }
    }
}
```

### 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.