2

Let me show a simplified example of the problem I'm struggling with:

class CarService {

    func getCars() -> Single<[Car]> {
        return Single.create { observer in
            // Here we're using a thread that was defined in subscribeOn().
            someCallbackToAPI { cars in
                // Here we're using main thread, because of the someCallbackToAPI implementation.
                observer(.success(cars))
            }
        }
    }
}

class CarRepository {

    func syncCars() -> Completable {
        return CarService().getCars()
            .flatMapCompletable { cars in
                // Here we're using main thread, but we want some background thread.
                saveCars(cars)
            }
    }
}

class CarViewController {

    func loadCar() {
        CarRepository().syncCars()
            .subscribeOn(someBackgroundScheduler)
            .observeOn(MainThread)
            .subscribe()
    }
}

From the bottom: CarViewController wants to sync all the cars from some external API. It defines what thread should be used for the sync with subscribeOn - we don't want to block the UI thread. Unfortunately, underneath, the CarService has to use some external library methods (someCallbackToAPI) that always returns the result in a main thread. The problem is that after receiving the result, all methods below like e.g. saveCars are called in the same main thread. saveCars may block the UI thread because it saves data to database. Of course I could add observeOn between threads between CarService().getCars() and flatMapCompletable, but I want the CarRepository to be dump and know nothing about the threads. It is the CarViewController responsibility to define working thread.

So my question is, is it a way I could get the scheduler passed in subscribeOn method and switch back to the scheduler after receiving the result from someCallbackToApi?

Nominalista
  • 4,632
  • 11
  • 43
  • 102

1 Answers1

0

The short answer is no.

As you surmise, the problem is that your someCallbackToAPI is routing to the main thread which is not what you wanted and there's nothing you can do about that short of re-writing someCallbackToAPI. If you are using Alamofire or Moya, I think they have alternative methods that won't call the closure on the main thread but I'm not sure. URLSession does not switch to the main thread so one idea would be to use it instead.

If you want the saveCars to happen on a background thread, you will have to use observeOn to push the computation back onto a background thread from main. The only thing subscribeOn will do is call someCallbackToAPI(_:) on a background thread, it cannot dictate what thread the function will call its closure on.

So something like:

func syncCars() -> Completable {
    return CarService().getCars()
    .observeOn(someBackgroundScheduler)
        .flatMapCompletable { cars in
            // Now this will be on the background thread.
            saveCars(cars)
    }
}

As a final note, an empty subscribe is a code smell. Any time you find your-self calling .subscribe() for anything other than testing purposes, you are likely doing something wrong.

Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • Hi Daniel, thanks for the answer. From the architecture point of view, deciding on which thread the callbacks will be launched on Alamofire/Moya level is not an ideal solution. The only solution I can think of is to pass the scheduler as a parameter to `getCars` method and switch to it after the callback is launched. BTW empty `subscribe()` is just for the example. – Nominalista Feb 07 '20 at 07:05
  • Yes, but the underlying problem is that Alamofire performs work on a background thread, then switches to the main thread to emit the result, then you are switching back to a background thread to continue working with he result. It would be far better to never involve the main thread in the first place. If Alamofire doesn't provide a means to do it, then use URLSession which does. – Daniel T. Feb 07 '20 at 11:14