RxSwift应用MVVM

目前高移动端应用的要求已经越来越高, 主要体现在:

  1. 越来越复杂的用户可操作页面;
  2. 多个页面一起承载繁琐的业务;
  3. 多个状态需要实时反映到应用界面.

这种背景下正是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, 对应的就是PublishSubjectReplaySubject.

而相对地, 函数返回的对象则只需要用非广播类型, 因为实际情况调用函数的回调都只是调用者自己需要subscribe. 如果这个回调的结果有必要转化成广播类型, 则应该通过调用者自己转换.

Service模块之属性

既然已经可以确定, Service属性中的Observable需要用广播类型, 那如何选择PublishSubjectReplaySubject?

先看两个类型的描述:

/// 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(有点绕, 出图表达).

view_model_structure.png

数据: Model

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

最终的MVVM结构

mvvm_structure.png

示例代码

)


持续更新...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容