Contents
Chào bạn đến với Fx Studio. Bài viết này sẽ trình bày về một khái niệm mới trong series RxSwift & RxCocoa. Đó là Binding Observables. Chúng ta sẽ tiến hành phần tích và mổ xẻ nó trong iOS project.
Để bắt đầu, bạn cần nắm qua cách để hiển thị dữ liệu trong UIKit với RxCocoa. Đây là bài tiếp nối của bài trước. Nếu bạn chưa biết về nó, thì có thể tham khảo ở link dưới.
Còn mọi thứ đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
-
- Xcode 12
- Swift 5.3
- RxSwift 5.0
Tạm thời, bạn chỉ cần chuẩn bị môi trường như vậy là ổn. Chúng ta vẫn sẽ demo trên iOS Project và bạn có thể dùng lại project của bài trước. Bạn sẽ thấy sự tiếp tục trong quán trình tìm hiểu thêm về vũ trị RxSwift này.
1. Binding Observables là gì?
Về khái niệm, Binding không phải là khái niệm mới trong giới lập trình iOS. Tuy nhiên, một điều khá buồn là Apple chưa có đưa ra một định nghĩa về nó cho nền tảng iOS. Còn với MacOS thì đã có từ lâu. Dó đó, hầu hết các dev đều tự có cách riêng của mình để giải quyết bài toán kết nối dữ liệu này.
Với RxSwift, đã cũng cấp các giải pháp đơn giản cho bài toán trên. Chắc bạn có thể đã từng làm rồi hoặc không để ý tới nó trong RxSwift. Nhưng không sao, mình sẽ giải thích chúng đơn giản nhất trong bài viết này.
1.1. Binding Observables
Bắt đầu câu chuyện ngày hôm nay, chúng ta sẽ tìm hiểu về mối quan hệ của 2 thực thể này. Đó là:
Trong đó,
- Producer : thực thể tạo ra giá trị
- Consumer : xử lý giá trị từ Producer
Có thể bạn sẽ liên tương tới mối quan hệ giữa Observable và Observer. Thì ở đây nó mang ý nghĩa giới hạn hơn trong xử lý trong code. Đó là Consumer không được phép return
về giá trị. Và nếu bạn suy nghĩ về Binding 2 chiều thì hãy để sau nha.
Còn về cơ bản cho áp dụng Binding bằng cách sử dụng bind(to:)
của đối tượng Observable tới 1 đối tượng nào đó. Tất nhiên, yêu cầu Consumer phải là kiểu ObserverType
.
ObserverType là các thực thể chỉ chấp nhận việc ghi (write-only) dữ liệu và chúng không thể subscribe được.
1.2. Observer Type
Qua trên, lại thêm một định nghĩa mới, đó là ObserverType. Mới nghe qua thì nghe khá mệt mỏi. Mình sẽ không tìm định nghĩa cụ thể cho nó. Nhưng bạn hãy nhớ lại các thực thể Subject đã học trước đây.
Subject = ObserverType + ObservableType
Nó vừa gởi vừa nhận được. Subject lại lưu trữ được dữ liệu nữa. Giúp cho bạn liên kết UI với dữ liệu. Ngoài ra, có thể đăng ký tới để kích hoạt những thực thể khác nếu cần.
Ngoài Subject, thì Relay cũng có thể sử dụng được với bind(to:)
.
Tóm tắt phần này. Bạn không cần phải xử lý dữ liệu và gán dữ liệu cho 1 đối tượng nào đó thông cái hàm subscribe
và closure truyền thống như trước đây nữa. Công việc của bạn sẽ được gói gọn vào bind(to:)
. Và chỉ có như vậy mà thôi!
2. Sử dụng Binding Observables
Ở trên, ta đã mô tả sơ về mối quan hệ Binding Observables rồi. Muốn dễ hiểu hơn thì hãy demo vào code. Demo này sẽ dùng lại project ở bài trước đó. Nếu bạn nào quên thì có thể truy cập lại đây:
-
- Link: checkout
- Thư mục:
/Examples/BasicRxSwift
Chúng ta vẫn làm việc với màn hình WeatherCityViewController và sẽ tiếp nối các công việc trong đó. Và cũng mô tả cho công việc chúng ta sắp thực hiện thì bạn xem qua hình sau:
Chúng ta sẽ tạo ra một nguồn phát dữ liệu duy nhất. Từ đó, dữ liệu sẽ được điều phối tới các UI Control trong giao diện của bạn. Cách điều phối này sẽ khác cách subscribe . Thay vì handle dữ liệu và gán cho cách thuộc tính của UI Control, thì chúng ta sẽ bind tới thẳng các thuộc tính của UI Control.
Việc hiển thị dữ liệu mới lên UI Control sẽ là tự động.
2.1. Tách nguồn
Ở trên, đã đề cập tới nguồn dữ liệu. Nên việc trước tiên cần làm là bạn tách nguồn dữ liệu. Và nguồn dữ liệu chính là kết quả nhận được từ API. API sẽ trả về một Observable, đó cũng là tất cả chúng ta cần lúc này.
Bạn hãy mở file WeatherCityViewController và truy cập tới function viewDidLoad. Tìm tới đoạn code của bài trước
searchCityName.rx.text.orEmpty .filter { !$0.isEmpty } .flatMap { text in return WeatherAPI.shared.currentWeather(city: text).catchErrorJustReturn(Weather.empty) } .observeOn(MainScheduler.instance) .subscribe(onNext: { weather in self.cityNameLabel.text = weather.cityName self.tempLabel.text = "\(weather.temperature) °C" self.humidityLabel.text = "\(weather.humidity) %" self.iconLabel.text = weather.icon }) .disposed(by: bag)
(Xoá cũng được, thêm code khác cũng được hoặc sửa lại cũng được. Tuỳ ý bạn!)
Bạn hãy chia tách nó ra một tí nào. Bắt đầu, bằng việc tạo một Observable search
, phụ trách việc gọi API và phát dữ liệu nhận được từ API lại.
let search = searchCityName.rx.text.orEmpty .filter { !$0.isEmpty } .flatMap { text in return WeatherAPI.shared.currentWeather(city: text).catchErrorJustReturn(Weather.empty) } .share(replay: 1) .observeOn(MainScheduler.instance)
Không có gì mới lạ hết. Xoá đi phần subscribe và thêm 1 dòng .share(replay: 1)
. Nhắc lại toán tử share, thì nó giúp cho việc toàn vẹn giá trị của một Observable khi có nhiều Subscriber đăng ký tới. Và tiết kiệm tài nguyên lẫn bộ nhớ khi không cần thiết phải gọi API nhiều lần cho nhiều Subscriber.
Toán tử
share
trên thì còn có ý nghĩa là không cần phải gọi lại việc request API khi có một subscription mới. Chỉ dùng lại kết quả mới nhất mà thôi.
Xong! Chúng ta đã có nguồn dữ liệu. Sang bước tiếp theo nào!
2.2. Binding to UI Control
Chúng ta đã có Observable rồi. Nó đóng vài trò là Producer trong muốn quan hệ này. Nhân vật còn thiếu chính là Consumer. Bạn hãy thêm đoạn code sau vào tiếp.
search.map { $0.cityName } .bind(to: cityNameLabel.rx.text) .disposed(by: bag)
Trong đó:
map
dùng để biến đổi cả đối tượngWeather
thành 1String
đơn giản mà thôibind
để đưa dữ liệu kia tới đúng đối tượng cần tớitext
nằm trong không gian củaUILable.rx
, nó là 1 kiểu ObserverType
Bạn hãy truy lùng nó trong thư viện xem text
là ai?
public var text: RxCocoa.Binder<String?> { get }
Lại thêm 1 khái niệm mới là Binder. Tạm thời để sau nha. Build app và test thử xem việc bind
đầu tiên đã ổn chưa. Còn nếu đã ổn rồi thì bạn quất luôn cho các UI còn lại.
search.map { "\($0.temperature) °C" } .bind(to: tempLabel.rx.text) .disposed(by: bag) search.map { $0.cityName } .bind(to: cityNameLabel.rx.text) .disposed(by: bag) search.map { "\($0.humidity) %" } .bind(to: humidityLabel.rx.text) .disposed(by: bag) search.map { $0.icon } .bind(to: iconLabel.rx.text) .disposed(by: bag)
Cũng không quá phức tạp nhỉ! Bạn tiếp tục build và test chúng xem dữ liệu đã hiển thị đầy đủ hết chưa.
3. Binder
3.1. Khái niệm
Đây là class để tạo ra các đối tượng mang tính chất là ObserverType. Nhằm để giúp việc bind
data của một Observable.
Trong không gian ReactiveX (Rx) của RxCocoa, thì ta có nhiều thuộc tính cho nhiều UI Control mang kiểu Binder này. Nó sẽ giúp liên kết chặt chẽ giữa property UI với dữ liệu của nguồn phát. Phản ứng lại với từng giá trị mà nó nhận được.
Ta lấy ví dụ cho UIProgressView, với 1 property Binder như sau:
public var progress: Binder<Float>
Và code xem dung lượng file được upload:
let progressBar = UIProgressBar() let uploadFileObs = uploadFile(data: fileData) uploadFileObs .map { sent, totalToSend in return sent / totalToSend } .bind(to: progressBar.rx.progress) .disposed(by: bag)
Khi lượng dung lượng được upload được gởi đi, thì sẽ nhận được giá trị. Tiến hành biến đổi nó thành Float và bind(to:)
tới thuộc tính progress
trong không gian Rx. Khi đó về mặt UI, nó sẽ tự cập nhật luôn trên thanh UIProgressView. Mà ta không cần phải xử lý gì nữa.
3.2. Custom Binder
Chúng ta thực hiện Custom Binder một cách đơn giản nhất thôi nha. Chỉ mở rộng thêm class có sẵn và cho nó gia nhập vào không gian của Rx. Công việc ta gôm các bước sau:
Bước 1: Xác định thuộc tính cần liên kết. Hiển nhiên đây là các thuộc tính bình thường của class
- Ta chọn WeatherCityViewController làm ví dụ cho cuộc vui này.
.title
là thuộc tính của ViewController đó, hiển thị tên của ViewController trên NavigationBar
Bước 2: Mở rộng không gian .rx
cho WeatherCityViewController
- Không gian
rx
thì là các phần extension của lớp Reactive. Bạn tạo 1 extension mới cho nó, theo như đoạn code sau - Chú ý
Base
chính là class của chúng ta muốn thêm vào không gianrx
extension Reactive where Base: WeatherCityViewController { //... }
Bước 3: Tạo Binder Property
- Về tên đặt thì bạn thích tên gì cũng được, ở đây mình chọn tên là
title
cho hack não - Và kiểu dữ liệu là
String
extension Reactive where Base: WeatherCityViewController { var title: Binder<String> { return Binder(self.base) { (vc, value) in vc.title = value } } }
Giải thích chút:
- Quan trọng là hành động gì mà bạn cài đặt trong khối lệnh khởi tạo
Binder
- Trong đó
- Tham số cho
Binder(_:)
làbase
. Nghĩa là chính là ViewController của chúng ta - Closure cung cấp với 2 tham số
target
là base, hay lúc này chính là ViewControllervalue
là giá trị nhận được khi Observable gọi hàmbind(to:)
- Thực hiện logic trong closure cho phù hợp
- Tham số cho
Bước 4: binding to UI
- Xác định Observable
- Xử lý dữ liệu phù hợp cho Binder
- Gọi hàm
bind(to:)
tới đối tượng Binder vừa tạo
search.map { $0.cityName } .bind(to: self.rx.title) .disposed(by: bag)
Đoạn code này đặt ở viewDidLoad, cùng với các đoạn code Binding vừa tạo ở phần trên. Bạn hãy build lại project và cảm nhận kết quả thay đổi nha.
Bạn chú ý, sẽ thấy Title của NavigationBar sẽ update theo những gì chúng ta gõ vào từ bàn phím.
Tạm kết
- Binding Observables là mối quan hệ giữa 2 thực thể
- Producer : thực thể tạo ra giá trị
- Consumer : xử lý giá trị từ Producer
- ObserverType thì chỉ nhận dữ liệu và không phát đi hay return lại bất cứ gì.
bind(to:)
là toán tử để đưa dữ liệu phát ra từ Observable tới trực tiếp thuộc tính/đối tượng nào có kiểu là ObserverType. Mà không cần xử lý bằng một closure nào hết.- Điều kiện yêu cầu là kiểu dữ liệu của phần tử Observable phát đi và kiểu dữ liệu của ObserverType đó phải giống nhau.
- Binder là class thuộc dạng ObserverType trong RxCocoa. Custom Binder cho thuộc tính của một class, để đưa dữ liệu trực tiếp lên đó. Và sử dụng được hàm
bind(to:)
Okay! Tới đây thì mình xin kết thúc bài viết này. Nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)