Need help to wrap my head around the concept of retry and share in RxSwift

so I can't really wrap my head around the concept of retry() and share() function in an RxSwift.Observable. Well, I think I may have some ideas about what they are but a certain cases got me questioning my understanding about them. So I have this test case:

func testOnViewDidLoad_WhenError_ShouldRetry3Times() throws {
    var actualRetryCount = 0
    let creditCardInfoProvider: CreditCardInfoProviderBlock = {
        actualRetryCount += 1
        return .error(RxCocoaURLError.unknown)
    }
    
    let viewModel = createViewModel(creditCardInfoProvider: creditCardInfoProvider)
    
    viewModel.onViewDidLoad()
    
    XCTAssertEqual(3, actualRetryCount)
}

The class is:

final class PaymentInfoViewModel {
    private(set) lazy var populateData = creditCardsRelay.asDriver()
    private let creditCardsRelay = BehaviorRelay<[CreditCardInfo]>(value: [])

    private let creditCardInfoProvider: () -> Observable<[CreditCardInfo]>
    init(creditCardInfoProvider: @escaping () -> Observable<[CreditCardInfo]>) {
        self.creditCardInfoProvider = creditCardInfoProvider
    }

    func onViewDidLoad() {
        .....
    }
}

My first question is: How come this works?

func onViewDidLoad() {
    Observable.just(())
        .flatMapLatest { [weak self] () -> Observable<[CreditCardInfo]> in
            guard let `self` = self else { return .empty() }
            return self.creditCardInfoProvider() }
        .retry(3)
        .do(onError: { [weak self] in
            self?.handleErrors(error: $0)
        })
        .bind(to: creditCardsRelay)
        .disposed(by: disposeBag)
}

But this doesn't (the result was one. Meaning it didn't get retried.)?

func onViewDidLoad(){
    creditCardInfoProvider()
        .retry(3)
        .do(onError: { [weak self] in
            self?.handleErrors(error: $0)
        })
        .bind(to: creditCardsRelay)
        .disposed(by: disposeBag)
}

My second question is: I have another relay that will be triggered by the same function. So I refactored it to be a shared observable like this:

func onViewDidLoad() {
    let sharedCardProvider = Observable.just(())
        .flatMapLatest { [weak self] () -> Observable<[CreditCardInfo]> in
            guard let `self` = self else { return .empty() }
            return self.creditCardInfoProvider() }
        .retry(3)
        .do(onError: { [weak self] in
            self?.handleErrors(error: $0)
        }).share()
        
    sharedCardProvider
        .bind(to: creditCardsRelay)
        .disposed(by: disposeBag)
    
    sharedCardProvider
        .map { !$0.isEmpty }
        .bind(to: addCreditCardButtonHiddenRelay)
        .disposed(by: disposeBag)
}

The thing is, the test becomes red with the result of actualRetryCount is 6. Deleting the share() function returned the same value (ie. 6 retries). So, that means it got called twice like a normal observable, not a shared one. Why does that happen?

For now, what I did was putting the emitting of the second relay in a .do(onNext:) block so that's not really the problem here. I am just confused about the behavior.

Thanks in advance.

Upvotes: 1

Views: 357

Answers (1)

Daniel T.
Daniel T.

Reputation: 33967

The first one calls creditCardInfoProvider() three times, while the second one only calls it once.

Remember, the retry only resubscribes which will cause the Observable to execute its observer closure again. In the first function, the retry is resubscribing to the Observable that just returned whereas the second function, the retry is resubscribing to the Observable that error returned.

If you did this instead:

        let creditCardInfoProvider: CreditCardInfoProviderBlock = {
            return Observable.create { observer in
                actualRetryCount += 1
                observer.onError(RxCocoaURLError.unknown)
                return Disposables.create()
            }
        }

Then the closure that is passed to create would get called every time a new subscription is made and you would see actualRetryCount increment as expected.


As for how share works... In order to understand share, you first have to understand hot vs cold observables.

A hot observable shares its events with multiple subscribers whereas a cold observable does not.

For example:

let random = Observable<Int>.create { observer in
    observer.onNext(Int.random(in: 0..<1000))
    observer.onCompleted()
    return Disposables.create()
}

The above is a cold observable (which is the default.) This means that every subscriber will get a different number emitted to it. If this was a network call, then every subscriber would cause a new network request.

If you want multiple subscribers to get the same data, you need to share the observable...

Upvotes: 1

Related Questions