Tìm hiểu RxSwift bài 3 - Traits
Bài đăng này đã không được cập nhật trong 7 năm
Tổng quát
Swift có hệ thống type mạnh mẽ có thể giúp tăng tính đúng đắng và ổn định của ứng dụng và làm cho việc sử dụng Rx trực quan và đơn giản hơn. Trait giúp cho việc giao tiếp và đảm bảo các observable sequence property giữa các tầng với nhau cũng như cung cấp các giải pháp phù hợp cho từng bài toán được đặt ra . Trait nhắm đến các use-case đặt biệt thay vì raw observable có thể sử dụng trong tất cả các trường hợp. Ví dụ như đường bộ thì xe thô sơ, xe máy, xe oto đều có thêt đi nhưng đường cao tốc thì chỉ cho phép oto sử dụng nhằm nâng cao hiệu suất sử dụng. Chính vì vậy, Trait có thể sử dụng hoặc không sử dụng đều được giống như các hàm map, reduce, filter hoàn toàn có thể thay thế bằng các vòng lặp C-style.
Trait là gì
Trait là một warpper struct với một read-only observable sequence property.
struct Single<Element> {
let source: Observable<Element>
}
struct Driver<Element> {
let source: Observable<Element>
}
Khi một trait được tạo ra, bạn có thể gọi .asObservable để chuyển nó lại thành observable sequence bình thường.
Các loại Trait
Single
Một single trait là một observable thay vì emit một chuỗi các sự kiện thì nó luôn luôn trả về một thuộc tính hoặc một error.
- Chỉ emit một element hoặc một error
- Không gây ra share side effect Môt use case phổ biến cho việc dùng Single là khi request HTTP, request này chỉ trả về response hoặc error. Single có thể dùng khi bạn chỉ quan tâm đến một element chứ ko cần biết tất cả element trong chuỗi event sequece.
Tạo Single
Cách tạo một Single cũng tương tự khi tạo Observable.
func getRepo(_ repo: String) -> Single<[String: Any]> {
return Single<[String: Any]>.create { single in
let task = URLSession.shared.dataTask(with: URL(string: "https://api.github.com/repos/\(repo)")!) { data, _, error in
if let error = error {
single(.error(error))
return
}
guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves),
let result = json as? [String: Any] else {
single(.error(DataError.cantParseJSON))
return
}
single(.success(result))
}
task.resume()
return Disposables.create { task.cancel() }
}
}
Sử dụng single vừa tạo
getRepo("ReactiveX/RxSwift")
.subscribe { event in
switch event {
case .success(let json):
print("JSON: ", json)
case .error(let error):
print("Error: ", error)
}
}
.disposed(by: disposeBag)
subcriber sẽ nhận được singleEvent là .success
chứa một element của Sing gle hoặc .error
.
Nếu bạn có một raw Observable sequence, bạn cũng có thể transform nó thành Single.
Completable
Completable là một biến thể cảu Observable mà nó chỉ có thể complete hoặc emit một error. Completable được đảm bảo không emit một element nào trong chuỗi sequence.
- Emits zero elements
- Emits a completion event or an error
- Doesn't share side effects
Tạo Completable
Việc tạo môt Completable được thực hiện như sau
func cacheLocally() -> Completable {
return Completable.create { completable in
// Store some data locally
...
...
guard success else {
completable(.error(CacheError.failedCaching))
return Disposables.create {}
}
completable(.completed)
return Disposables.create {}
}
}
Sử dụng completable vừa tạo
cacheLocally()
.subscribe { completable in
switch completable {
case .completed:
print("Completed with no error")
case .error(let error):
print("Completed with an error: \(error.localizedDescription)")
}
}
.disposed(by: disposeBag)
Subcriber sẽ nhận một CompletableEvent enum có thể là .completed
chứng tỏ việc thực hiện đã hoàn thành mà không có lỗi xảy ra, hoặc .error
Maybe
Maybe và sự kết hợp của Single và Completable. Nó có thể emit một element, complete hoặc error. Chú ý: Bất cứ trạng thái event nào emit ra cũng sẽ kết thúc Maybe, điều này có nghĩa nêu một Maybe đã emit một element thì Maybe đó không thể gởi completed hoặc error event. Chúng ta sử dụng Maybe nếu một operation có thể emit một element nhưng không nhất thiết phải luôn luôn emit một element.
Tạo maybe
func generateString() -> Maybe<String> {
return Maybe<String>.create { maybe in
maybe(.success("RxSwift"))
// OR
maybe(.completed)
// OR
maybe(.error(error))
return Disposables.create {}
}
}
Sự dụng Maybe vừa tạo
generateString()
.subscribe { maybe in
switch maybe {
case .success(let element):
print("Completed with element \(element)")
case .completed:
print("Completed with no element")
case .error(let error):
print("Completed with an error \(error.localizedDescription)")
}
}
.disposed(by: disposeBag)
RxCocoa traits - các loại trait trong RxCocoa
Driver
Đây là loại trait phức tạp nhấtnhất. Driver được sử dụng để việc viết code react ở UI một cách thuận tiện hơn, hoặc khi bạn muốn xử lý dữ liệu
- Can't error out
- Observe occurs on main scheduler
- Shares side effects (shareReplayLatestWhileConnected)
Vì sao lại là Driver
Driver sử dụng trong trường hợp dữ liệu của bạn được truyền từ lớp này sang lớp khác.
- Drive UI từ dữ liệu CoreData
- Drive UI sử dụng dữ liệu từ UI element khác ( ví dụ bạn nhập text từ textfield và hiển thị giá trị tức thời lên một label). Trong trường hợp sequence gởi một error, app sẽ dừng nhận input từ use. Lý thuyết ở đây quả thực khó hiểu- mình không thể dịch cho các bạn rõ ràng được, cùng nhau học nó qua ví dụ sau đây:
let results = query.rx.text
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
}
results
.map { "\($0.count)" }
.bind(to: resultCount.rx.text)
.disposed(by: disposeBag)
results
.bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.disposed(by: disposeBag)
Đoạn code trên sẽ làm các công việc sau:
- Điều chỉnh user input
- Fetch kết quả từ input của user
- Bind kết qủa nên result count và table view Đoạn code trên có rất nhiều vấn đề chưa được xử lý như
- Chưa xử lý lỗi khi fetch dữ liệu từ server
- Chưa kiểm tra việc update UI trên main thread
- Kết quả được bind đến 2 UI elements cho nên với mỗi query từ user, 2 http request sẽ được tạo cho mỗi UI element. Chúng ta có thể viết đoạn code trên lại như sau:
let results = query.rx.text
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.observeOn(MainScheduler.instance) // Kết quả sẽ được trả về trên main
.catchErrorJustReturn([]) // nếu lỗi xảy ra sẽ trả về mảng rỗng
}
.shareReplay(1) // request được share giữa các UI element
results
.map { "\($0.count)" }
.bind(to: resultCount.rx.text)
.disposed(by: disposeBag)
results
.bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.disposed(by: disposeBag)
Tuy nhiện, chúng ta có thể sử dụng Driver để viết code ngon hơn trong trường hợp trên.
let results = query.rx.text.asDriver() // Yea, chuyển sequence trên thành Driver
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.asDriver(onErrorJustReturn: []) // Cho biết khi error thì cần xử lý như thế nào
}
results
.map { "\($0.count)" }
.drive(resultCount.rx.text) // If there is a `drive` method available instead of `bindTo`,
.disposed(by: disposeBag) // that means that the compiler has proven that all properties
// are satisfied.
results
.drive(resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.disposed(by: disposeBag)
Đầu tiên, asDriver
đã convert ControlProperty
trait thành Driver
trait.
query.rx.text.asDriver()
Driver có tất cả thuộc tính của ControlProperty
trait và thêm vào:
.asDriver(onErrorJustReturn: [])
Bất cứ observable sequence nào cũng có thể convert qua Driver nếu nó thoả mãn 3 điều sau:
- Can't error out
- Observe on main scheduler
- Sharing side effects (shareReplayLatestWhileConnected)
Làm sao bạn biết được khi nào thì các điều kiện trên thoả mãn và bạn có thể thay Rx operator bình thường thành Driver? Hãy xem so sánh
asDriver(onErrorJustReturn: [])
với code dưới đây.
let safeSequence = xs
.observeOn(MainScheduler.instance) // observe events on main scheduler
.catchErrorJustReturn(onErrorJustReturn) // can't error out
.shareReplayLatestWhileConnected() // side effects sharing
return Driver(raw: safeSequence) // wrap it up
Điểm đặc biệt cuối cùng của Driver là sử dụng drive
thay vì bindto
drive
chỉ được định nghĩa với Driver. Điều này đồng nghĩa với khi bạn thấy drive
thì block code ở đó sẽ không nhận mã lỗi gởi ra và luôn luôn lắng nghe trên mainthread.
Tổng kết
Chúng ta đã kết thúc bài 3 trong series cùng tìm hiểu RxSwift. Hẹn gặp lại các bạn vào kì sau.
All rights reserved