嗨嗨,接續昨天範例的部分,今天繼續往下走,介紹ViewModel的部分,我想不管是MVVMC或是MVVM,ViewModel都不擇了資料跟邏輯的部分,ViewController負責邏輯的部分,所以在兩者之間,使用Rx來做binding。
以Pure Function概念出發,我們也可以把ViewModel分為Input跟Output,Input就是會進到邏輯處理資料或事件;Output就是可能會給Controller接收或是Coordinator接收的Observable。
宣告變數部分可以分為Input跟Output
// MARK: - Input
let apiService: ProductAPIProtocol
let triggerAPI: AnyObserver<Void>
let triggerNextPage: AnyObserver<Void>
// MARK: - Output
let didClose: Observable<FinishReason>
let isLoading: Observable<Bool>
let data: Observable<[Product]>
let page: Observable<Int>
let showError: Observable<Error>
// MARK: - Private
private let disposeBag = DisposeBag()
Input的部分
Output的部分
Constructor部分就會包含變數的定義,
// MARK: - Constructor
init(apiService: ProductAPIProtocol) {
self.apiService = apiService
didClose = .never()
.never()
let pageRelay = BehaviorRelay<Int>(value: 1)
page = pageRelay.asObservable()
let productListRelay = BehaviorRelay<[Product]>(value: [])
data = productListRelay.asObservable()
let indicator = ActivityIndicator()
isLoading = indicator.asObservable()
let triggerAPISubject = PublishSubject<Void>()
triggerAPI = triggerAPISubject.asObserver()
let triggerNextPageSubject = PublishSubject<Void>()
triggerNextPage = triggerNextPageSubject.asObserver()
pageRelay
跟productListRelay
分別是目前頁面與產品的資料,使用Relay是因為在串接過程中要加減一頁,或是新增或清空陣列,用Relay很方便。triggerAPISubject
和triggerNextPageSubject
作為中介者,對外開放Observer接收到訊息,接收到之後,對內作為Observable發送給訂閱者,這對應到第 5 天 - Subject (上) - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天所講的Subject特性,但同樣的,為了避免被到處.onNext()
,這裡只對外開放Observer。let fetchProductListResult = triggerAPISubject.asObservable().startWith(())
.flatMapLatest { _ in apiService.request(page: 1).trackActivity(indicator).materialize() }
.share()
fetchProductListResult.elements()
.bind { products -> Void in
pageRelay.accept(1)
productListRelay.accept(products)
}
.disposed(by: disposeBag)
triggerAPISubject
的.next
事件後,去call API,並回傳回來的資料,這對應到第 11 天 - Transforming Observables(下) - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天所說的,因為一進畫面需要先載入一次資料,所以用startWith
先觸發一次fetchProductListResult.elements()
這整段則是在處理改變pageRelay
的數值為1,以及將productListRelay
覆蓋為新的資料。let nextProductListResult = triggerNextPageSubject.asObservable()
.debounce(.seconds(2), scheduler: MainScheduler.instance)
.flatMapLatest { _ in apiService.request(page: pageRelay.value+1).trackActivity(indicator).materialize() }
.share()
nextProductListResult.elements()
.bind { products -> Void in
let newPage = pageRelay.value + 1
let newData = productListRelay.value + products
pageRelay.accept(newPage)
productListRelay.accept(newData)
}
.disposed(by: disposeBag)}
.next(Void)
,這不是我們預期的,所以這裡放置debounce
來過濾掉頻繁的元素nextProductListResult.elements()
這段仍是處理pageRelay
跟productListRelay
,我們將數值先暫存,更新後再存放回去。showError = Observable.merge(fetchProductListResult.errors(),
nextProductListResult.errors())
最後將所以可能會發生錯誤的地方,匯集到showError
,在由要呈現錯誤訊息的地方去訂閱。
目前這樣寫雖然一看就懂,但缺點就是有點冗,總感覺nextPage跟reload是可以合併在一起,礙於道行還不夠,或許有大大經過可以提點一下,又或是我三個月後再來看,又會有新的感覺吧!明天講講測試,RxTest跟RxBlocking,掰掰