RxSwift by Examples

Dựa theo tài liệu từ TheDroidSonroids Swift là loại ngôn ngữ khá linh hoạt và dễ dàng sử dụng. Vì vậy bạn có thể thấy swift ko chỉ đc sử dụng trong lập trình hướng đối tượng mà cũng đc sử dụng trong nhiều mô hình khác như Protocol-Oriented Programming mà đã đc giới thiệu ở WWDC'15. Và dĩ nhiên bạn ko hề phải mất công tìm kiếm để có thể thấy 1 điều rằng swfit cũng được sử dụng trong Functional ProgrammingReactive Programming. Vậy thì Functional Reactive Programming nghĩa là gì? Lập trình phản ứng hướng chức năng? - khó hiểu vcl =)) tuy nhiên theo tôi, hiểu đơn giản thì nó sử dụng Reactive Programming với các blocks của Functional Programming (như filter, map, reduce,...) Và điều thú vị là Swift đã có sẵn những thứ đó, còn về phần Reactive, RxSwift sẽ lo hết. RxSwift là 1 phiên bản Reactive Extensions được viết bằng Swift.

ReactiveX is a combination of the best ideas from the Observer pattern, the Iterator pattern, and functional programming

Một cách cơ bản, bạn thay đổi cái góc nhìn, cách làm việc của mình từ việc assign tĩnh 1 giá trị cho 1 biến sang việc: "quan sát" (observe) một cái gì đó mà nó có thể thay đổi trong tương lai. Và bạn có thể sẽ hỏi:"tại sao phải thay đổi? Tại sao ko làm theo cách cũ? tại sao tao phải sử dụng cách làm này?" - Câu trả lời rất đơn giản: bởi vì nó đang là mốt! =)) và quan trọng hơn là nó làm công việc của bạn đơn giản đi rất nhiều. Thay vì sử dụng notifications - rất khó để test - chúng ta sử dụng signals. THay vì dùng delegate - mà tốn rất nhiều dòng code và code lại còn đặt rải rác khắp nơi - chúng ta có thể viết blocks và bỏ qua hoàn toàn việc switches/ifs. Chúng ta cũng có KVO, IBActions, input filters, MVVM và rất rất nhiều thứ khác mà được xử lý vô cùng dễ dàng với RxSwift. Tuy nhiên hãy nhớ, RxSwift k phải luôn luôn là cách tốt nhất để giải quyết vấn đề, nhưng bạn cần phải biết kha khá về nó để sử dụng nó với tiềm năng cao nhất.

Definitions

CHúng ta sẽ bắt đầu với 1 số định nghĩa, để hiểu hơn về mặt logic của nó, chúng ta cần đi qua những thứ cơ bản. Chiếc điện thoại của bạn là một observable. Nó emits signals(phát ra các tín hiệu) như Notifications của facebook, tin nhắn, cuộc gọi đến,... Và bạn - 1 cách tự nhiên - subscribed tới cái điện thoại, vì thế bạn nhận đc tất các các notification đó ở màn hình home của điện thoại. Và bây giờ bạn có quyền quyết định sẽ làm gì với các signal đó - vì thế bạn là một observer. EZ

Example.

Chúng ta sẽ viết 1 app gọi là City Searcher - viết tên city vào search box và show ra list city cho chúng ta 1 cách dynamically. Khi bạn viết vào search bar, nghĩa là bạn đang cố gắng fetch những thành phố mà tên bắt đầu bằng những chữ cái bạn đang nhập vào. vô cùng ez. Khi bạn muốn 1 dynamic search, bạn phải luôn luôn nghĩ tới những thứ gì có thể gây ra lỗi? Ví dụ như: user viết quá nhanh và thay đổi liên tục -> chúng ta sẽ phải liên tục request API lên server-> ko hợp lý. Ở app thực tế, bạn sẽ phải cancel requests cũ và chời 1 chút để gửi request mới lên, check lại người dùng nhập mới vào có trùng với lúc trước không,... đôi khi, nó tạo ra 1 khối logic vô cùng lớn mà ban đầu cứ nghĩ là dễ dàng. Dĩ nhiên bạn có thể làm đc điều đó mà không cần RxSwift, nhưgn hãy cứ thử xem:

Đầu tiên, bạn hãy tạo 1 project mới, sau đó install CocoaPods và RxSwift + RxCocoa. file Podfile sẽ trông như sau:

platform :ios, '8.0'
use_frameworks!
 
target 'RxSwiftExample' do
 
pod "RxSwift"
pod "RxCocoa"
 
end

Mở file Main.storyboard và thêm vào 2 UI đơn giản là UISearchBarUITableView Tiếp theo chúng ta cần 1 array để giữ các giá trị về cities. Để giảm thiểu phẩn logic ko cần thiết, chúng ta sẽ bỏ qua API và sử dụng 2 array: 1 array chứa tất cả các city và 1 array chứa các city được show ra.

var shownCities = [String]() // Data source for UITableView
let allCities = ["New York", "London", "Oslo", "Warsaw", "Berlin", "Praga"] // Our mocked API data source

Tiếp theo, chúng ta sẽ setup UITableViewDataSource và connect nó với showCities như sau

@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: UISearchBar!
 
override func viewDidLoad() {
    super.viewDidLoad()
    tableView.dataSource = self
}
 
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return shownCities.count
}
 
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cityPrototypeCell", for: indexPath)
    cell.textLabel?.text = shownCities[indexPath.row]
    
    return cell
}

Như vậy bây giờ nếu chúng ta thay đổi giá trị của shownCities, chúng ta sẽ thấy nó thể hiện trên màn hình. Bây giờ là phần thú vị nhất: Chúng ta sẽ observe đối tượng text của UISearchBar. NÓ thực sự rất dễ dàng bởi vì nó đã được built-in ở trong RxCocoa (extension của RxSwift). UISearchBar và rất nhiều controls khác của cocoa đều được support bởi Rx team. Trong trường hợp này, để sử dụng được UISearchBar chúng ta sử dụng property rx.text của nó, property này sẽ phát ra signal mỗi khi text của search bar thay đổi. Vậy làm sao để observe nó? Đầu tiên, bạn cần import 2 thư viện:

import RxCocoa
import RxSwift

Trong hàm viewDidLoad() chúng ta sẽ thêm việc observe thuộc tính rx.text như sau:

searchBar
    .rx.text // Observable property thanks to RxCocoa
    .orEmpty // Make it non-optional
    .subscribe(onNext: { [unowned self] query in // Here we will be notified of every new value
        self.shownCities = self.allCities.filter { $0.hasPrefix(query) } // We now do our "API Request" to find cities.
        self.tableView.reloadData() // And reload table view data.
    })
    .addDisposableTo(disposeBag)

Note: As you can see in RxSwift 3.1 instead of prefix rx_something (which was in earlier versions and Swift 2.2) there is rx.something. Thanks to that it is more convenient to check what properties/methods exist for given objects. Additionally there is no subscribeNext etc. syntax. However you can use my pod RxShortcuts and get it back with other helpful functions.

Như vậy công việc dynamic search của chúng ta đã thành công tốt đẹp. subscribeNext khá là dễ hiểu: chúng ta đăng ký để theo dõi 1 property, mà property đó sẽ phát ra signal. Nó giống như việc bạn nói với cái điện thoại của mình: "đc rồi, mỗi khi có gì mới thì hãy show ra cho tao xem" Và nó sẽ show các notification lên màn hình home cho bạn. Trong trường hợp của chúng ta, chúng ta cần ko chỉ là mỗi khi có giá trị gì đó mới mà còn cả những event như onError inCompleted,..

Điều thú vị hơn nữa ở những dòng code trên đó là dòng cuối cùng. Khi bạn đăng ký theo dõi 1 observable dĩ nhiên, bạn sẽ muốn ngưng theo dõi nó khi nó bị deallocated. Trong Rx, chúng ta có 1 thứ gọi là DisposeBag mà sẽ được dùng để giữ những thứ mà bạn muốn unsubscribe khi deinit được gọi. Trong 1 số trường hợp nó là không cần thiết, nhưng luật chung là bạn sẽ luôn luôn phải tạo ra 1 cái bag như vậy và add disposables cho nó:

var shownCities = [String]() // Data source for UITableView
let allCities = ["New York", "London", "Oslo", "Warsaw", "Berlin", "Praga"] // Our mocked API data source
let disposeBag = DisposeBag() // Bag of disposables to release them when view is being deallocated

Và bây giờ, sau khi compiling, chúng ta sẽ thấy app chạy đúng như ta đã kỳ vọng. Nhưng còn những vấn đề mà chúng ta lo sợ lúc trước như: API spamming, Empty phrase, delay,... Chúng ta cần phải có 1 chút delay, mà sẽ gọi request sau khoảng X seconds sau khi người dùng type xong, chỉ khi nội dung type ko bị thay đổi. Bình thường chúng ta có thể sử dụng NSTimer để request sau 1 khoảng thời gian, hoặc invalidate nó nếu như 1 phrase mới được type. Không quá khó, nhưng vẫn dễ xảy ra lỗi. Nếu chúng ta type vào chữ "O", kêt quả xuất hiện, chúng ta chuyển thành "Oc" nhưng ngay lập tức quay về "O" ngay trước khi delay và thế là 1 request đc tạo ra giống hết như request trước đó. Ấy nhưng trong 1 số trường hợp ta cần điều này nếu như database đc refresh liên tục nhưng thường thì nó là ko cần thiết phải gọi 2 request giống hệt nhau giữa khoảng thời gian 0.5 giây. Nếu không dùng Rx chúng ta sẽ cần phải có 1 flag / last searched query để so sánh với query mới. Nhưng với RxSwift, nó sẽ đơn giản như sau:

searchBar
    .rx.text // Observable property thanks to RxCocoa
    .orEmpty // Make it non-optional
    .debounce(0.5, scheduler: MainScheduler.instance) // Wait 0.5 for changes.
    .distinctUntilChanged() // If they didn't occur, check if the new value is the same as old.
    .subscribe(onNext: { [unowned self] query in // Here we subscribe to every new value
        self.shownCities = self.allCities.filter { $0.hasPrefix(query) } // We now do our "API Request" to find cities.
        self.tableView.reloadData() // And reload table view data.
    })
    .addDisposableTo(disposeBag) 

Tuyệt vời, nhưng chúng ta lại quên 1 điều: nếu như user viết gì đó, refresh table view, rồi delete hết để tạo ra 1 phrase rỗng? chúng ta sẽ send 1 query với empty parameter... chúng ta cần phải tránh điều này bằng cách dùng filter() và bạn sẽ hỏi: "Thế éo nào tao lại phải dùng filter cho 1 value? filter dùng trong collections!!!" - good question =)) nhưng bạn đừng suy nghĩ Observable như 1 value/object. Nó là một luồng các giá trị stream of values, mà sẽ xảy ra khá thường xuyên. Vì thế bạn sẽ dẽ dàng hiểu những dòng dưới đây:

searchBar
    .rx.text // Observable property thanks to RxCocoa
    .orEmpty // Make it non-optional
    .debounce(0.5, scheduler: MainScheduler.instance) // Wait 0.5 for changes.
    .distinctUntilChanged() // If they didn't occur, check if the new value is the same as old.
    .filter { !$0.isEmpty } // If the new value is really new, filter for non-empty query.
    .subscribe(onNext: { [unowned self] query in // Here we subscribe to every new value, that is not empty (thanks to filter above).
        self.shownCities = self.allCities.filter { $0.hasPrefix(query) } // We now do our "API Request" to find cities.
        self.tableView.reloadData() // And reload table view data.
    })
    .addDisposableTo(disposeBag)