642
Demo MVVM với RxSwift:
Model Layer:
struct Category {
let id: Int
var name: String
}
Class Service để thao tác với database:
import Foundation
protocol ICategoryService {
func getCategoryById(id: Int) -> Category?
}
class CategoryService: ICategoryService {
func getCategoryById(id: Int) -> Category? {
let categories = [Category(id: 3, name: "Movies"),
Category(id: 4, name: "Books"),
Category(id: 5, name: "Computer")]
let category = categories.filter({
$0.id == id
}).first
return category
}
}
ViewModel Layer
import Foundation
import RxSwift
class HomeViewModel {
var category: Category
var service: ICategoryService
var displayName: String {
return transformName(category.name)
}
// 1
var valueSubject: PublishSubject<String>
init(category: Category, service: ICategoryService) {
self.category = category
self.service = service
valueSubject = PublishSubject()
}
func transformName(_ name: String) -> String {
let newName = name.enumerated().map { (index, character) -> String in
if index % 2 == 0 {
return character.uppercased()
} else {
return character.lowercased()
}
}
return newName.joined()
}
// 2
func getCategory(id: Int) {
if let newCategory = service.getCategoryById(id: id) {
self.category = newCategory
valueSubject.onNext(transformName(category.name))
}
}
}
- Khởi tạo 1 subject kiểu Publish Subject để phát ra các event khi giá trị của category’s name được thay đổi.
- ViewModel sẽ thông qua Model Layer để truy xuất đến database. Sau khi lấy được data cần thiết, thì valueSubject sẽ phát ra 1 event có giá trị là name đã được transform của category mới.
View Layer:
import UIKit
import RxSwift
class HomeView: UIViewController {
@IBOutlet private weak var categoryNameLabel: UILabel!
private var viewModel: HomeViewModel?
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let category = Category(id: 1, name: "Hollywood")
viewModel = HomeViewModel(category: category, service: CategoryService())
// 1
self.categoryNameLabel.text = viewModel?.displayName
// 2
viewModel?.valueSubject
.skip(1)
.subscribeOn(MainScheduler.instance)
.subscribe(onNext: { (displayName) in
self.categoryNameLabel.text = displayName
})
.disposed(by: disposeBag)
}
@IBAction func didTapButtonChangeCategory(_ sender: Any) {
// 3
viewModel?.getCategory(id: 3)
}
}
- Set text cho label lần đầu khi khởi tạo category.
- Lắng nghe khi viewModel phát ra 1 value mới.
- Tap button để yêu cầu viewModel lấy ra 1 category có id = 3.
Tác dụng của MVVM:
- Tương tự như MVP, lợi ích đầu tiên của MVVM là tách biệt phần logic ra khỏi View. Từ đó dẫn đến dễ viết unit test, dễ maintain, …
- Dễ dàng reuse các ViewModel.
- Bằng việc tương tác với View thông qua cơ chế data binding, vì vậy không cần tạo thêm nhiều protocol, class…
MVVM vs MVP:
- Trong MVVM, vì ViewModel tương tác với View bằng data binding, vì vậy ViewModel không có 1 reference nào đến View. Từ đó ViewModel sẽ dễ dàng được reuse, dễ dàng viết test hơn so với MVP.
- Trong MVVM, 1 View có thể có nhiều ViewModel.
- MVVM sẽ không cần phải tạo nhiều protocol như MVP.
- Việc sử dụng cơ chế data binding cũng dẫn đến 1 hệ quả là MVVM sẽ khó để debug hơn nhiều so với việc sử dụng MVP.
- Nếu sử dụng RxSwift kết hợp với MVVM, thì sẽ phải đòi hỏi team của bạn đều phải biết RxSwift ở mức ổn.
Kết luận:
- Mục tiêu chính của MVVM là tách biệt logic khỏi View. Tuy nhiên, những phần logic như truy vấn database, networking, … thì chưa được xử lí. Những logic như vậy có thể được đặt ở presenter, hoặc tạo 1 class riêng ở Model tùy theo bản thân bạn.
Tuy nhiên, nên tách biệt các phần logic Database, networking, … ra các class riêng để tuân thủ nguyên tắc Single Responsibility Principle của SOLID. - Theo nguyên tắc MVVM thì ViewModel không import UIKit để tách biệt logic và View.