iOS · reactive programming · ReactiveX

Sử dụng retry và retryWhen trong ReactiveX

Trong một vài tình huống, chúng ta muốn thực thi lại một tác vụ khi có lỗi xảy ra, chẳng hạn như kết nối tới máy chủ. Trong quá trình phát triển ứng dụng cho công ty, mình thực hiện phần này, nhân tiện giới thiệu một số toán tử trong ReactiveX, giúp chúng ta thực thi lại một số tác vụ khi có lỗi xảy ra.

func createObservable() -> Observable {
    return Observable.create { o
        // thực hiện logic
        return Disposables.create()
    }
}

Tình huống 1: Tự động retry

createObservable().retry().subscribe()

Nếu có lỗi khi thực thi createObservable, hàm retry sẽ được gọi. Khi đó, hàm createObservable và các chuỗi toán tử (nếu có) sẽ được thực thi lại cho tới khi chuỗi tác vụ thực thi thành công.

Việc retry cho tới khi nào thành công chứa đựng nhiều rủi ro, vì có thể gây ứng dụng chạy không ngừng, nên chúng ta thường giới hạn số lần retry như ví dụ sau: hàm createObservable sẽ được retry tối đa ba lần.

createObservable().retry(3).subscribe()

Tình huống 2: Trì hoãn một khoảng thời gian trước khi retry

Trong một số trường hợp, chúng ta muốn trì hoãn (delay) một khoảng thời gian trước khi thực hiện retry. Ví dụ khi thực hiện kết nối mạng lỗi,  chúng ta trì hoãn một khoảng thời gian trước khi retry, việc này cho phép máy chủ hoặc hạ tầng mạng có thêm một chút thời gian để phục hồi.

createObservable()
.retryWhen { (attempts: Observable) -> Observable in
    attempts.delay(TimeInterval(0.2), scheduler: MainScheduler.instance)
}

Tương tự như hàm retry, ở ví dụ trên observable và chuỗi toán tử sẽ được thực thi cho tới khi chuỗi tác vụ thực thi thành công. Chúng ta có thể sử dụng scan để giới hạn số lần retry.

Tình huống 3: Phát ra tín hiệu thành công sau khi retry một số lần nhất định

Ở các ví dụ trên, nếu quá trình retry thất bại, chuỗi observable sẽ phát ra tín hiệu error cho observer. Đôi khi, chúng ta muốn phát ra tín hiệu thành công cho observer như ở ví dụ sau:  createObservable sẽ được retry tối đa hai lần. Sau hai lần retry, nếu không thành công thì sẽ phát tín hiệu thành công cho observer.

.createObservable()
.retryWhen { (attempts: Observable) -> Observable in
    attempts.scan(0) { $0.0 + 1 }.takeWhile { $0 <= 2}
}

Tình huống 4: 

Ở các ví dụ trên, các tác vụ được thực thi lại tự động khi có lỗi xảy ra, không có sự tương tác với người dùng. Việc này có thể khiến người dùng chờ đợi một khoảng thời gian dài mới nhận được phản hồi.

Chúng ta có thể hiển thị một popup như hình bên dưới, ứng dụng sẽ tự động retry sau ba giây. Sau ba lần retry tự động, ứng dụng không tự động retry, nhưng cho phép người dùng retry hoặc huỷ bỏ tác vụ (hình bên phải). Ngoài ra, chúng ta không retry cho tất cả các lỗi, mà chỉ áp dụng cho một số lỗi nhất định, thường là lỗi kết nối mạng.

Trước hết, chúng ta thêm mới protocol, RetryDelegate, việc này cho phép chúng ta có thể retry ở mọi nơi (view, service, view model, …)

protocol RetryDelegate {
    func retryWhen(attempts: Observable, filter: ErrorFilter) -> Observable
}

Tiếp theo, chúng ta thực hiện RetryHandler thừa kết từ protocol vừa tạo ra.

final class RetryHandler: RetryDelegate {
    struct ScanState {
        let count: Int
        let error: Error?
    }

    func retryWhen(attempts: Observable, filter: ErrorFilter) -> Observable {
        return attempts.scan(ScanState(count: 0, error: nil)) { 
           ScanState(count: $0.0 + 1, error: $0.1)
        }.flatMap { state -> Observable in
            guard let error = state.error else {return Observable.just()}
            if !filter.valid(error: error) { throw error }
            return self.handleError(error:error,retriedCount:state.count)
        }
    }

    private func handleError(error: Error, retriedCount: Int) -> Observable {
        return Observable.create { observer in
            if self.sharedPopupController == nil {
                self.sharedPopupController = RetryViewController(autoRetry: retriedCount <= 3)
            }

            self.sharedPopupController?.addRetryHandler {
                self.sharedPopupController = nil
                observer.onNext()
                observer.onCompleted()
            }

            self.sharedPopupController?.addCancelHandler {
                self.sharedPopupController = nil
                observer.onError(error)
            }

            self.presentRetryModal()
            return Disposables.create()
        }
    }

    private func presentRetryModal() {
        guard let controller = sharedPopupController else { return }
        if controller.presentingViewController == nil {
            UIViewController.topMostController?.present(controller, animated: true, completion: nil)
        }
    }
    private weak var sharedPopupController: RetryViewController?
}

Chúng ta có thể sử dụng các lớp trên như sau:

let retryDelegate = RetryHandler()
let errorFilter = NetworkErrorFilter()

createObservable()
.retryWhen { (error: Observable) -> Observable in
    return retryDelegate.retryWhen(attempts: error, filter: errorFilter)
}

errorFilter cho phép chúng ta chỉ định một số lỗi nhất định sẽ được áp dụng retry, các lỗi còn lại không được retry.

Xem toàn bộ ví dụ tại đây

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Google photo

Bạn đang bình luận bằng tài khoản Google Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s