Tìm hiểu RxSwift bài 3 - Traits

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.