07/09/2018, 17:16

RxSwift: Bài 6: RxCocoa (Part 3) - Binding Observables

RxSwift: Bài 6: RxCocoa (Part 3) - Binding Observables Một điều quan trọng cần biết ở đây là trong RxCocoa, binding là một luồng dữ liệu đơn hướng (unidirectional stream of data). Điều này làm đơn giản hóa lưu lượng dữ liệu trong ứng dụng vì vậy bạn sẽ không cần lo đến các ràng buộc hai chiều ...

RxSwift: Bài 6: RxCocoa (Part 3) - Binding Observables

Một điều quan trọng cần biết ở đây là trong RxCocoa, binding là một luồng dữ liệu đơn hướng (unidirectional stream of data). Điều này làm đơn giản hóa lưu lượng dữ liệu trong ứng dụng vì vậy bạn sẽ không cần lo đến các ràng buộc hai chiều (bi- directional bindings) ở đây.

1. What are binding observables?
Hãy tưởng tượng mối quan hệ của radio, máy phát và nhận tín hiệu:

alt text

1 producer giống như là máy phát tín hiệu, 1 receiver sẽ xử lý giá trị từ producer.

Điều quan trọng nhất: 1 receiver không trả về bất kì dạng nào, đây là luật chung khi sử dụng bindings của RxSwift

alt text

Hàm chính của binding chính là hàm bind(to:). Để bind 1 observable đến 1 thực thể khác, receiver phải comform ObserverType. Thực thể này đã nói ở phần trước: đó là 1 Subject có thể xử lý tín hiệu lẫn nhập dữ liệu cho nó. Subject rất quan trọng khi làm việc với những phần bắt buộc của Cocoa, kiểu như những component quen thuộc như UILabel, UITextField, UIImageView có data thay đổi có thể set hoặc truy xuất.

Thực ra, bind(to:) cũng có thể được sử dụng cho các mục đích khác chứ không riêng gì bind UI với data ở dưới. Ví dụ, bạn có thể sử dụng bind(to:) để tạo ra các dependent processes (các tiến trình hỗ trợ), từ đó observable này sẽ có thể trigger 1 subject để làm một vài background tasks mà không hiển thị bất kì cái gì lên trên màn hình.

Nói chung, bind(to: ) là 1 phiên bản đặc biệt và dễ chịu của subscribe(to: ), không có ảnh hưởng phụ hay trường hợp đặc biệt khi call bind(to: )

2. Using binding observables to display data
Sau khi tìm hiểu bindings là gì, bạn có thể bắt đầu tích hợp chúng vào trong app. Bạn sẽ thấy code dễ nhìn hơn và có thể dùng lại được.

Trước hết, ta refactor đoạn code cũ này, nó gắn data đến những label tương ứng thông qua subscribe(onNext:):

// code cũ
searchCityName.rx.text
  .filter { ($0 ?? "").characters.count > 0 }
  .flatMap { text in 
return ApiController.shared.currentWeather(city: text ?? "Error")
    .catchErrorJustReturn(ApiController.Weather.empty)
}

Vào trong viewDidLoad() và thay thế đoạn code sau:

let search = searchCityName.rx.text
  .filter { ($0 ?? "").characters.count > 0 }
  .flatMapLatest { text in
    return ApiController.shared.currentWeather(city: text ?? "Error")
        .catchErrorJustReturn(ApiController.Weather.empty)
  }
  .observeOn(MainScheduler.instance)

Trong phần thay đổi này, đặc biết là flatMapLatest, giúp kết quả search có thể được sử dụng nhiều lần và chuyển từ data sử dụng 1 lần thành 1 Observable sử dụng nhiều lần. Bạn sẽ thấy sự khác biệt khi bước qua phần MVVM nhưng hiện tại bây giờ, bạn chỉ cần thấy observables là một reusable entities rất mạnh trong Rx. Nó chính xác là một mô hình có thể chuyển 1 Observer dùng 1 lần, khó đọc dài ngoằng thành 1 observer dễ hiểu, dễ dùng và dễ đọc.

Đây là mô hình nó sẽ hoạt động:
alt text

Với chỉ 1 cái thay đổi nhỏ này, nó có thể xử lý mọi parameter từ 1 subscription, sau đó map giá trị cần thiết để hiển thị. Ví dụ, để lấy giá trị nhiệt độ từ 1 chuỗi string ra khỏi data source observable:

 search.map { "($0.temperature)° C" }

Dòng code này sẽ tạo 1 observable mà trả lại 1 required string để display cái temperature. Thử tạo binding đầu tiên bằng cách sử dụng bind(to: ) để kết nối data source đến temperature label, thêm dòng code sau:

search.map { "($0.temperature)° C" }
  .bindTo(tempLabel.rx.text)
  .addDisposableTo(bag)

Build và run thử, kết quả sẽ là:
alt text

App bây giờ chỉ hiển thị mỗi nhiệt độ nhưng bạn có thể lưu lại và sử dụng func đó bằng cách áp dụng same pattern với các labels kia:

search.map { $0.icon }
  .bindTo(iconLabel.rx.text)
  .addDisposableTo(bag)

search.map { "($0.humidity)%" }
  .bindTo(humidityLabel.rx.text)
  .addDisposableTo(bag)

search.map { $0.cityName }
  .bindTo(cityNameLabel.rx.text)
  .addDisposableTo(bag)

Bây giờ thì nó đã hiển thị data mà bạn yêu cầu từ server, sử dụng duy nhất 1 Observable tên là search và binds nhiều phần data cho từng label tương ứng trên màn hình:

alt text

Một điểm hay và tiện nữa đó là việc check bởi compiler để đoạn bảo việc sử dụng những loại type 1 cách chính xác. Cơ bản thì không thể có các loại khác nhau hoàn toàn mà làm crash app được. Thực sự đoạn này mình không hiểu lắm, nguyên văn trong documentation là:

Another nice, clean addition is the check made by the compiler to ensure the usage of the correct kind of types. It’s basically impossible to have completely disparate types that crash the app.

Note: một phần note rất quan trọng là khi binding đến 1 UIComponents, RxCocoa sẽ check để đảm bảo việc quan sát được perform trên main thread. Nêú không, nó sẽ gọi 1 fatalError() và application sẽ crash với message là:
fatal error: Element can be bound to user interface only on MainThread.

0