+3

Tìm hiểu về RxSwift

Reactive programming với ý tưởng chính là ứng dụng của bạn có thể phản ứng với những thay đổi trong dữ liệu cơ bản mà không cần ta phải trực tiếp gọi đến nó. Nhờ đó ta có thể tập trung vào logic hiện tại hơn là bảo trì một trạng thái nhất định.

Điều này có thể đạt được trong Objective-C hay Swift thông qua Key-Value Observertion và sử dụng didSet trong Swift, nhưng những phương thức này đôi khi khá cồng kềnh để thiết lập một cách chính xác. Để tránh vấn đề này, có một vài frameworks trong cả Objective-C và Swift được tạo ra để tạo thuận lợi cho việc sử dụng reactive programming. Bài viết này sẽ giới thiệu về RxSwift, một trong những framework đó.

RxSwift là gì?

RxSwift là một phiên bản Reactive Extension được viết bằng ngôn ngữ Swift. ReactiveX là sự kết hợp của những ý tưởng hay nhất từ Observer pattern, Iterator pattern và functional programming.

RxSwift sẽ giúp công việc của bạn trở nên đơn giản hơn. Thay cho notifications, một đối tượng khó để test, ta có thể sử dụng signals. Thay cho delegates, thứ tốn rất nhiều code, ta có thể viết blocks và bỏ đi switches/ifs lồng nhau. Ta còn có thể sử dụng KVO, IBActions, filters, MVVM và nhiều tiện ích khác được hỗ trợ mượt mà trong RxSwift.

Ví dụ

Hãy cùng bắt đầu làm quen với RxSwift bằng một ví dụ nhỏ sau đây. Demo này cung cấp chức năng tìm kiếm thành phố, user gõ vào tên thành phố trong search box và sẽ filter để hiển thị list thành phố một cách dynamic theo từ khoá nhập vào. Khi user nhập giá trị vào search bar, app sẽ fetch những thành phố có tên bắt đầu bằng những keyword đó và hiển thị lên table view. Đối với dynamic search bạn có thể phải khá đau đầu để xử lý những request thay đổi liên tục và quá nhanh, có thể có rất nhiều API request ta cần filter. Có khi ta phải huỷ request trước, đợi một thời gian trước khi gởi request khác, kiểm tra tính trùng lặp với request trước đó.... Rx sẽ giúp ta viết những logic này với rất ít code và thậm chí không cần dòng code nào.

Trước tiên, tạo project đặt tên CitySearching, sau đó cài CocoaPods và RxSwift, RxCocoa trong Podfile.

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

Ta tạo một view controller đơn giản chứa một search control (UISearchBar) và một table view (UITableView) trong Main.storyboard. Tạo mới file view controller SearchingCityViewController và khai báo mảng shownCities chứa các thành phố được tìm ra, và mảng allCities chứa tất cả các thành phố. Ta sẽ sử dụng mảng data này thay vì gọi API để giảm bớt logic cần xử lý.

var shownCities = [String]() // Data source for UITableView
let allCities = ["Oklahoma", "Chicago", "Moscow", "Danang", "Vancouver", "Praga"] // Mocked API data source

Kết nối outlet từ storyboard vào SearchingCityViewController và cung cấp data source cho tableview:

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

Nếu chạy project lên, màn hình sẽ hiển thị như sau:

Tiếp theo, ta sẽ theo dõi text user nhập vào ô search. Điều này vô cùng dễ dàng nhờ RxCocoa (extention của RxSwift) đã được xây dựng sẵn. UISearchBar cũng như rất nhiều controls khác trong Cocoa framework đã hỗ trợ cho Rx. Đối với UISearchBar, ta có thể sử dụng thuộc tính rx.text, nó sẽ phát ra tín hiệu một khi text điền vào ô search được thay đổi. Đầu tiên, hãy import RxCocoa và RxSwift vào:

import RxCocoa
import RxSwift

Trong viewDidLoad(), ta thêm vào đoạn code sau để theo dõi thuộc tính rx.text của UISearchBar:

searchBar
    .rx.text // Observable property
    .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)

Trong Rx, disposeBag là nơi chứa tất cả những thứ mà ta muốn bỏ đăng ký theo dõi trong qua quá trình deinit. Nếu đối tượng observed đã được giải phóng khỏi bộ nhớ thì ta cũng không cần theo dõi nữa. Ta có thể khởi tạo disposeBag như sau:

let disposeBag = DisposeBag() // Bag of disposables to release them when view is being deallocated

Bây giờ, nếu chạy project lên, ta đã có thể search được kết quả ngay sau khi gõ vào chữ cái đầu tiên, thật tuyệt phải không.

Tuy nhiên, khi sử dụng mà ta liên tục gọi API khi có bất cứ thay đổi nào như vậy sẽ tạo ra rất nhiều request lên server. Vì vậy việc thêm delay time trước khi gọi API là điều cần thiết. Rx cung cấp phương thức debounce() giúp ta dễ dàng tạo ra delay time theo thời gian được định sẵn (thông thường ta sẽ dùng NSTimer). và distinctUntilChanged() sẽ bảo vệ app khỏi việc gởi request cho những giá trị giống nhau. Đoạn code search lúc này sẽ giống như sau:

searchBar
    .rx.text // Observable property
    .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) 

Trong trường hợp người dùng gõ từ khoá vào, table sẽ hiển thị kết quả, và sau đó, người dùng lại xoá hết từ khoá tìm kiếm đi và ô search bây giờ sẽ trống, lúc này, app vẫn sẽ gởi request với parameter rỗng, điều này thực sự không cần thiết. Vì vậy, ta có thể thêm vào điều kiện chỉ filter cho những query không rỗng như sau:

.filter { !$0.isEmpty }

Đoạn code cuối cùng sẽ như sau:

searchBar
    .rx.text // Observable property
    .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)

Kết luận

Trên đây chỉ là những kiến thức cơ bản về RxSwift, bạn có thể tìm hiểu thêm về Rx trên trang chủ http://reactivex.io và nhiều thông tin hơn về RxSwift tại https://github.com/ReactiveX/RxSwift. Chúc các bạn thành công.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.