目前高移动端应用的要求已经越来越高, 主要体现在:
- 越来越复杂的用户可操作页面;
- 多个页面一起承载繁琐的业务;
- 多个状态需要实时反映到应用界面.
这种背景下正是MVVM模式施展的时候, 而MVVM模式下的好搭档无疑要提到响应式框架.以下把个人在工作中对RxSwift&MVVM实践的经验记录一下.
数据流中心模块: Service
我在项目中构建了一个Service模块, 作为数据流的中心, 这个模块的作用有以下几点:
单例模式, 项目共享数据与状态.
提供业务/功能相关的函数, 内部调用下一层异步函数, 例如 Network API, Location API, 第三方SDK API, Database API等操作.
以属性的形式提供全局可观察事件, 例如 User登录状态变更, Location 更新.
Service模块之Observable
在Service模块中, 大部分函数的返回值和属性的类型都是Observable.
Observable是Rx框架中的最基本可观察类型, 其中有一个需要用到的概念——广播型.
广播类型和非广播类型的区别在于, 广播类型可以同时多次subsribe, 这个特性正好契合全局可观察事件. 因此Service模块中提供的Observable属性都应该是广播类型的. 而Rx框架里面的广播类型Observable, 对应的就是PublishSubject和ReplaySubject.
而相对地, 函数返回的对象则只需要用非广播类型, 因为实际情况调用函数的回调都只是调用者自己需要subscribe. 如果这个回调的结果有必要转化成广播类型, 则应该通过调用者自己转换.
Service模块之属性
既然已经可以确定, Service属性中的Observable需要用广播类型, 那如何选择PublishSubject和ReplaySubject?
先看两个类型的描述:
/// Represents an object that is both an observable sequence as well as an observer.
///
/// Each notification is broadcasted to all subscribed observers.
public final class PublishSubject<Element>
: Observable<Element>
, SubjectType
, Cancelable
, ObserverType
, SynchronizedUnsubscribeType {
...
...
}
/// Represents an object that is both an observable sequence as well as an observer.
///
/// Each notification is broadcasted to all subscribed and future observers, subject to buffer trimming policies.
public class ReplaySubject<Element>
: Observable<Element>
, SubjectType
, ObserverType
, Disposable {
...
...
}
从注释上面可以了解到, ReplaySubject的不同在于, 它的数据会发送给将来订阅它的监听者, 也就是说, ReplaySubject的数据可以追溯.
可以得出ReplaySubject的使用策略:
当数据发送的时机早于数据被正式使用的时候, 我们用ReplaySubject, 这样可以避免我们丢失已经获得的数据.
Service模块之函数
在设想中, 我希望Service是MVVM中的ViewModel的辅助, ViewModel调用Service提供的函数, 而Service应该帮ViewModel处理好与UI无关的业务逻辑. 所以Service的函数应该有大量异步串行/并行的操作, 并且返回Observable类型.
剩下需要注意的就是, 函数的副作用. 这一点非常值得注意, 因为稍不注意副作用的管理, 就会导致ViewModel收到一些莫名的数据更新. 对此, 人为约束:
- 有副作用的函数, 会导致
Service持有的数据改变, 或者导致Service的可观察对象发送数据的, 使用以下命名:- 会返回数组数据的函数,
reloadxxx表示重新刷新,loadMorexxx表示加载更多. - 返回
hash数据/Model的函数, 使用updatexxx表示更新.
- 会返回数组数据的函数,
- 没有副作用的函数:
- 请求数据的, 无论返回什么类型, 使用
fetchxxx表示请求. - 执行某类操作, 使用
performxxx. 例如登录, 命名应该是performSignin.
- 请求数据的, 无论返回什么类型, 使用
ViewModel和View
MVVM有别于MVC主要在于ViewModel模块, 设想中它的功能有以下:
- 存储
View/ViewController层要直接显示的数据, 注意不只是持有Model. - 针对对应的
View/ViewController提供UI交互的输入口(Tableview, TextField, Button, Segmented等操作). - 内部处理UI的input数据, 调用对应
Service的业务函数, 并提供输出口绑定到View/ViewController上. - 映射
Service模块的一些可观察事件, 输出给View/ViewControllersubscribe.
class SignViewControllerViewModel {
// input
var usernameInput: BehaviorRelay<String> = BehaviorRelay(value: "")
var passwordInput: BehaviorRelay<String> = BehaviorRelay(value: "")
// output
var usernameInputEnable: Observable<Bool>
var passwordInputEnable: Observable<Bool>
var submitEnable: BehaviorRelay<Bool> = BehaviorRelay(value: true)
var submitTitle: BehaviorRelay<String> = BehaviorRelay(value: "Submit")
var indicatorHidden: Observable<Bool>
var stateHidden: Observable<Bool>
var state: Observable<String> {
return _internalState.asObservable()
}
...
...
init(disposeBag: DisposeBag!) {
...
}
func handleClickSubmit() {
...
}
}
MVVM中的V是视图层, 在项目中视图层主要其实是ViewController和各种View的子类, 并不是单独指View.
而View/ViewController要做的就是做布局, 以及调用ViewModel:
class SignViewController: UIViewController {
// 懒加载, 传入disposeBag
var viewModel: SignViewControllerViewModel
// UI
@IBOutlet weak var usernameTf: UITextField!
@IBOutlet weak var passwordTf: UITextField!
@IBOutlet weak var submitButton: UIButton!
@IBOutlet weak var stateLabel: UILabel!
@IBOutlet weak var indicatorView: UIActivityIndicatorView!
// deinit之后释放subscribtions
let disposeBag: DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
...
...
self.bindObservables()
}
func bindObservables() {
// 数据输入 viewModel
self.usernameTf.rx.text.orEmpty
.bind(to: self.viewModel.usernameInput)
.disposed(by: self.disposeBag)
self.passwordTf.rx.text.orEmpty
.bind(to: self.viewModel.passwordInput)
.disposed(by: self.disposeBag)
self.submitButton.rx.tap
.subscribe(onNext: {_ in self.viewModel.handleClickSubmit()})
.disposed(by: self.disposeBag)
// viewModel 数据输出
self.viewModel.submitEnable
.bind(to: self.submitButton.rx.isEnabled)
.disposed(by: self.disposeBag)
self.viewModel.usernameInputEnable
.bind(to: self.usernameTf.rx.isEnabled)
.disposed(by: self.disposeBag)
self.viewModel.passwordInputEnable
.bind(to: self.passwordTf.rx.isEnabled)
.disposed(by: self.disposeBag)
self.viewModel.submitTitle
.bind(to: self.submitButton.rx.title())
.disposed(by: self.disposeBag)
self.viewModel.state
.bind(to: self.stateLabel.rx.text)
.disposed(by: self.disposeBag)
self.viewModel.indicatorHidden
.bind(to: self.indicatorView.rx.isHidden)
.disposed(by: self.disposeBag)
self.viewModel.indicatorHidden
.map({!$0})
.bind(to: self.indicatorView.rx.isAnimating)
.disposed(by: self.disposeBag)
self.viewModel.stateHidden
.bind(to: self.stateLabel.rx.isHidden)
.disposed(by: self.disposeBag)
}
}
在理想情况下, View/ViewController只面对ViewModel, 而ViewModel面对的就是Service, Model, View/ViewController
注意: 因为ViewController中必定存在若干个View, 而View是可以复用的, 甚至ViewController也是可以作为childViewController被复用, 所以ViewController的应该有一个对应的ViewModel, 而ViewController的ViewModel有可能需要持有若干个View的ViewModel(有点绕, 出图表达).

数据: Model
Model在MVVM中是最轻的一个模块, 除了保存数据的值, 它什么都不需要做, 所见即所有.
最终的MVVM结构

示例代码
)
持续更新...