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 theViewModel
, while UI click events are defined as closures. - Establish a
Mediator
that, upon completion of the API request, initializes allViewModel
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 theMediator
.
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 thePageMediator
. - ##### Utilize a
MediatorManager Singleton
for eachPageMediator
(in the H5 demo, can useVuex
). 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 appropriatedesign patterns
to refactor thePageMediator
. The specific approach to splitting depends on individual preferences and architectural capabilities.It's crucial to carefully consider
public
variables
andmethods
withinPageMediator
. 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 forUI Test
. For instance, modules like Router, Toast, and Network. For example, forToast
:
class ToastViewModel {
// use ReactiveX replace setter
set toast(value) {
Toast(value)
}
}
IV. Code Implementation
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 beunit test
in place ofUI 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)