Giới thiệu về thư viện RXSwift
This post hasn't been updated for 8 years
Chào các bạn! Dạo gần đây tôi tìm hiểu về reactive programming và tôi tìm thấy thư viện rxswift rất hay mà tôi muốn giới thiệu với các bạn.
Tại sao nên sử dụng Rx
Lợi ích của Rx
- Rx cho phép xây dựng app theo Declarative Programing*
- Composable : chương trình là sự kết hợp của nhiều component(thành phần), các thành phần này độc lập và có khả năng thay thế mà không ảnh hướng tới các thành phần khác.
- Reusable : code có khả năng tái sử dụng cao Dễ hiểu và chính xác vì Rx nâng cao mức trừu tượng (abstraction) và loại bỏ các trạng thái trung gian.
- Rx đã được thực hiện unit test nên rất ổn định
- Rx mô hình hóa ứng dụng như là unidirectional data flows (các luồng dữ liệu một hướng) do vậy sẽ giảm stateful*
- Không có memory leak <- Quản lý tài nguyên dễ dàng
Bindings
Observable.combineLatest(firstName.rx_text, lastName.rx_text) { $0 + " " + $1 }
.map { "Greetings, \($0)" }
.bindTo(greetingLabel.rx_text)
Nó cũng hoạt động với UITableViews and UICollectionViews.
viewModel
.rows
.bindTo(resultsTableView.rx_itemsWithCellIdentifier("WikipediaSearchCell", cellType: WikipediaSearchCell.self)) { (_, viewModel, cell) in
cell.title = viewModel.title
cell.url = viewModel.url
}
.addDisposableTo(disposeBag)
Nên luôn luôn sử dụng .addDisposableTo(disposeBag) cho dù dòng lệnh này là không cần thiết đối với trường hợp đơn giản
Retries
Giả sử ta có một API như sau:
func doSomethingIncredible(forWho: String) throws -> IncredibleThing
Làm thế nào để có thể chạy lại hàm này trong trường hợp có lỗi xảy ra? Tất nhiên bạn có thể sử dụng mô hình phức tạp như Exponential backoffs* nhưng trong code sẽ có nhiều trạng thái tạm thời không cần thiết và code cũng không thể tái sử dụng. Việc này được thực hiện hết sức đơn giản với Rx
doSomethingIncredible("me")
.retry(3)
Bạn cũng có thể dễ dàng custom lệnh retry này.
Delegates
Thay vì đoạn code không mấy ấn tượng:
public func scrollViewDidScroll(scrollView: UIScrollView) { [weak self] // what scroll view is this bound to?
self?.leftPositionConstraint.constant = scrollView.contentOffset.x
}
... hãy viết
self.resultsTableView
.rx_contentOffset
.map { $0.x }
.bindTo(self.leftPositionConstraint.rx_constant)
KVO
Nếu không có Rx bạn sẽ gặp trường hợp khi mà đối tượng đã được giải phóng nhưng KVO vẫn đang gắn với nó. Khi đó sẽ xảy ra leak hoặc thậm chí KVO được gắn nhầm với đối tượng khác Hãy sử dụng rx_observe và rx_observeWeakly như sau:
view.rx_observe(CGRect.self, "frame")
.subscribeNext { frame in
print("Got new frame \(frame)")
}
hoặc
someSuspiciousViewController
.rx_observeWeakly(Bool.self, "behavingOk")
.subscribeNext { behavingOk in
print("Cats can purr? \(behavingOk)")
}
Notifications
Thay vì:
@available(iOS 4.0, *)
public func addObserverForName(name: String?, object obj: AnyObject?, queue: NSOperationQueue?, usingBlock block: (NSNotification) -> Void) -> NSObjectProtocol
... chỉ cần viết
NSNotificationCenter.defaultCenter()
.rx_notification(UITextViewTextDidBeginEditingNotification, object: myTextView)
.map { /*do something with data*/ }
....
Transient state
Khi lập trình không đồng bộ sẽ có rất nhiều transient state (trạng thái tạm thời). Hãy xét ví dụ với autocomplete search box. Khi lập trình không sử dụng Rx bạn sẽ gặp các vấn đề sau: Bạn muốn tìm abc. Khi gõ đến c thì request đối với ab vẫn ở trạng thái chờ chưa được thực hiện và do đó request đối với ab cần được hủy. Để giải quyết vấn đề này bạn cần tạo thêm một biến để giữ reference tới request của “ab” Khi request thất bại bạn cần thêm retry logic cũng như khai báo số lần retry Để tránh trường hợp server bị spam , người dùng gõ một đoạn text rất dài chẳng hạn, bạn cần có một timer để tạo thời gian delay trước khi gửi request tới server. Thêm câu hỏi nữa được đặt ra đó là sẽ hiển thị cái gì lên màn hình khi mà quá trình search đang được thực hiện? Hay khi đã retry nhiều lần mà vẫn có lỗi ? Xử lý tất cả các vấn đề nêu trên sẽ rất đơn giản với Rx mà không cần thêm biến hay cờ nào.
searchTextField.rx_text
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest { query in
API.getSearchResults(query)
.retry(3)
.startWith([]) // clears results on new search term
.catchErrorJustReturn([])
}
.subscribeNext { results in
// bind to ui
}
**Compositional disposal **
Xét một ví dụ: Hiển thị ảnh được làm nhòe lên trên table view. Ảnh được lấy về từ URL, decode, làm nhòe. Quá trình làm nhòe ảnh sẽ tốn băng thông cũng như thời gian xử lý. Làm thế nào để hủy toàn bộ quá trình này khi một cell không còn nằm trong màn hình nữa.
Khi một cell xuất hiện trên màn hình bạn không nên cho phép ảnh được tải về ngay lập tức bởi vì khi người dùng vuốt màn hình rất nhanh, sẽ có rất nhiều request được gửi đi và bị hủy bỏ. Làm thế nào để đạt được điều này? Làm thế nào để có thể giới hạn số xử lý ảnh đồng thời bởi xử lý ảnh sẽ mất rất nhiều thời gian? Bạn có thể thực hiện tất cả với Rx:
// this is a conceptual solution
let imageSubscription = imageURLs
.throttle(0.2, scheduler: MainScheduler.instance)
.flatMapLatest { imageURL in
API.fetchImage(imageURL)
}
.observeOn(operationScheduler)
.map { imageData in
return decodeAndBlurImage(imageData)
}
.observeOn(MainScheduler.instance)
.subscribeNext { blurredImage in
imageView.image = blurredImage
}
.addDisposableTo(reuseDisposeBag)
Đoạn code trên sẽ thực hiện tất cả và khi imageSubscription được dispose, chương trình sẽ hủy bỏ tất cả các xử lý không đồng bộ phụ thuộc và đảm bảo không có ảnh nào được đưa lên UI.
Aggregating network requests
Làm thế nào để gửi đi 2 request và tổng hợp kết quả của cả 2 khi chúng kết thúc? Với Rx chúng ta sử dụng zip để thực hiện việc này
let userRequest: Observable<User> = API.getUser("me")
let friendsRequest: Observable<Friends> = API.getFriends("me")
Observable.zip(userRequest, friendsRequest) { user, friends in
return (user, friends)
}
.subscribeNext { user, friends in
// bind them to the user interface
}
Trường hợp bạn muốn các API trả về kết quả ở background thread và đưa lên UI main thread ta sẽ sử dụng observeOn.
let userRequest: Observable<User> = API.getUser("me")
let friendsRequest: Observable<[Friend]> = API.getFriends("me")
Observable.zip(userRequest, friendsRequest) { user, friends in
return (user, friends)
}
.observeOn(MainScheduler.instance)
.subscribeNext { user, friends in
// bind them to the user interface
}
State
Việc thay đổi global state của app nếu không cẩn thận sẽ dẫn đến combinatorial explosion. Combinatorial explosion là khi số lượng trạng thái của app tăng rất nhanh theo hàm mũ khiến tốn rất nhiều thời gian để xử lý. Để tránh xảy ra combinatorial explosion Rx giữ state đơn giản nhất có thể và sử dụng unidirectional data flow cho derived data. Rx cho phép sử dụng các định nghĩa và hàm bất biến để bắt các trạng thái biến đổi Easy integration Tự tạo observer với RxCocoa
extension NSURLSession {
public func rx_response(request: NSURLRequest) -> Observable<(NSData, NSURLResponse)> {
return Observable.create { observer in
let task = self.dataTaskWithRequest(request) { (data, response, error) in
guard let response = response, data = data else {
observer.on(.Error(error ?? RxCocoaURLError.Unknown))
return
}
guard let httpResponse = response as? NSHTTPURLResponse else {
observer.on(.Error(RxCocoaURLError.NonHTTPResponse(response: response)))
return
}
observer.on(.Next(data, httpResponse))
observer.on(.Completed)
}
task.resume()
return AnonymousDisposable {
task.cancel()
}
}
}
}
All Rights Reserved