Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Bye bye RxJava: Building flexible SDKs with Kotlin and Coroutines

Bye bye RxJava: Building flexible SDKs with Kotlin and Coroutines

We've made some mistakes building our client-facing SDKs over the last two years at Babylon Health.

RxJava has been around in Android development for years, and we've learned a great deal about it. It's a powerful tool, but with great power comes great responsibility. And really - you should always use the right tool for the job, not the most "powerful" one. Now we have a new tool in our toolbox - Coroutines - and they seem better suited to a typical Android business layer. It doesn't mean RxJava should be entirely out of the window, as for us it's still quite useful on the UI layer.

We're here to share why and how we went from a heavily RxJava based SDK architecture to a more client and developer-friendly approach replacing most of our code with Kotlin & Coroutines and compare both solutions.

We will admit to mistakes, share the principles that guided us in our planning for fixing them and show you how we came up with a plan and executed it.

Mikolaj Leszczynski

September 09, 2019
Tweet

More Decks by Mikolaj Leszczynski

Other Decks in Programming

Transcript

  1. Step 2 - clean architecture UI Network stream Presenter commands

    actions Repository Use case stream stream
  2. SDK Step 4 - fully reactive (experiment*) UI Network request


    stream MVI state stream action stream Repository Use case response
 stream stream response
 stream request
 stream
  3. Callback APIs data class GetPatientRequest(val patientId: String) interface GetPatientOutput {

    fun onGetPatientSuccess(patient: Patient) } interface BabylonUserApi { fun getPatient( request: GetPatientRequest, output: GetPatientOutput ): Disposable }
  4. Rx APIs // From RxJava sources public interface ObservableTransformer<Upstream, Downstream>

    { ObservableSource<Downstream> apply(Observable<Upstream> upstream); }
  5. Rx APIs sealed class GetPatientStatus { data class Ready(val patient:

    Patient) : GetPatientStatus() object Loading : GetPatientStatus() data class Error(val throwable: Throwable) : GetPatientStatus() }
  6. Rx APIs internal class GetPatientUseCase ( ... ) : ObservableTransformer<GetPatientRequest,

    GetPatientStatus>() { override fun apply( upstream: Observable<GetPatientRequest> ): Observable<GetPatientStatus> = upstream.switchMap { patientRepository.getPatient(it.patientId) .map { patient -> GetPatientStatus.Ready(patient) } .onErrorReturn { throwable -> GetPatientStatus.Error(throwable) } .startWith(GetPatientStatus.Loading) } }
  7. Rx APIs upstream.switchMap { patientRepository.getPatient(it.patientId) .map { patient -> GetPatientStatus.Ready(patient)

    } .onErrorReturn { throwable -> GetPatientStatus.Error(throwable) } .startWith(GetPatientStatus.Loading) }
  8. Rx APIs upstream.switchMap { patientRepository.getPatient(it.patientId) .map { patient -> GetPatientStatus.Ready(patient)

    } .onErrorReturn { throwable -> GetPatientStatus.Error(throwable) } .startWith(GetPatientStatus.Loading) }
  9. Rx APIs upstream.switchMap { patientRepository.getPatient(it.patientId) .map { patient -> GetPatientStatus.Ready(patient)

    } .onErrorReturn { throwable -> GetPatientStatus.Error(throwable) } .startWith(GetPatientStatus.Loading) }
  10. class GetImportantMessagesUseCase @Inject constructor( private val loadSessionConfigurationUseCase: LoadSessionConfigurationUseCase, private val

    getAppointmentsUseCase: GetAppointmentsUseCase, private val getLoggedInPatientUseCase: GetLoggedInPatientUseCase, private val getNotificationsUseCase: GetNotificationsUseCase, private val isHealthCheckCompletedUseCase: IsHealthCheckCompletedUseCase, private val systemUtil: SystemUtil ) : AuthenticatedUseCase<GetImportantMessagesRequest, GetImportantMessagesStatus>() { override fun applyUseCase(upstream: Observable<GetImportantMessagesRequest>): Observable<GetImportantMessagesStatus> { return upstream.switchMap { Observable.just(GetLoggedInPatientRequest()) .compose(getLoggedInPatientUseCase) .toReadyData() .toObservable() .switchMap { patient -> Observable.combineLatest<LoadSessionConfigurationStatus, GetAppointmentsStatus, GetNotificationsStatus, IsHealthCheckCompletedStatus, GetImportantMessagesStatus>( Observable.just(LoadSessionConfigurationRequest()) .compose(loadSessionConfigurationUseCase), Observable.just(GetAppointmentsRequest(patient.id !!)) .compose(getAppointmentsUseCase), Observable.just(GetNotificationsRequest) .compose(getNotificationsUseCase), Observable.just(IsHealthCheckCompletedRequest) .compose(isHealthCheckCompletedUseCase), Function4 { sessionConfigurationStatus, getAppointmentsStatus, getNotificationsStatus, isHealthCheckCompletedStatus -> when { getAppointmentsStatus is GetAppointmentsStatus.Error -> GetImportantMessagesStatus.Error(getAppointmentsStatus.throwable) sessionConfigurationStatus is LoadSessionConfigurationStatus.Error -> GetImportantMessagesStatus.Error(sessionConfigurationStatus.throwable) getNotificationsStatus is GetNotificationsStatus.Error -> GetImportantMessagesStatus.Error(getNotificationsStatus.throwable) isHealthCheckCompletedStatus is IsHealthCheckCompletedStatus.Error ->
  11. SDK Before - Fully reactive SDK UI Network request
 stream

    MVI state stream action stream Repository Use case response
 stream stream response
 stream request
 stream
  12. SDK After - V2 SDK architecture UI Network Call MVI

    State stream action stream Repository Use case
  13. Lower layers as simple as possible internal interface PaymentCardsService {

    @GET(PAYMENT_CARD_ENDPOINT) suspend fun getPaymentCards(): List<PaymentCardModel> } Retrofit 2.6.0+
  14. Lower layers as simple as possible internal interface PaymentCardsRepository {

    suspend fun getPaymentCards(): List<PaymentCard> }
  15. Lower layers as simple as possible class NetworkPaymentCardsRepository( private val

    paymentCardsService: PaymentCardsService ) : PaymentCardsRepository { override suspend fun getPaymentCards(): List<PaymentCard> = paymentCardsService.getPaymentCards() .map { it.toDomainEntity() } }
  16. interface Call<T> { fun execute(): T fun enqueue(callback: Callback<T>) fun

    cancel() } interface Callback<T> { fun onResult(data: T) fun onException(throwable: Throwable) }
  17. class PaymentV2ApiImpl( private val paymentCardsRepository: PaymentCardsRepository ) : PaymentV2Api {

    override fun getPaymentCards() = wrapCall { paymentCardsRepository.getPaymentCards() } }
  18. class PaymentV2ApiImpl( private val paymentCardsRepository: PaymentCardsRepository ) : PaymentV2Api {

    override fun getPaymentCards() = wrapCall { paymentCardsRepository.getPaymentCards() } }
  19. Before class GetPrescriptionsUseCase ( private val prescriptionsRepository: PrescriptionsRepository, private val

    userAccountsRepository: UserAccountsRepository ) : AuthenticatedUseCase<GetPrescriptionsRequest, GetPrescriptionsStatus>() { override fun applyUseCase(upstream: Observable<GetPrescriptionsRequest>): Observable<GetPrescriptionsStatus> { return upstream.switchMap { request -> getPrescriptions(request) .toObservable() .map<GetPrescriptionsStatus> { GetPrescriptionsStatus.Ready(request.location, it) } .startWith(GetPrescriptionsStatus.Loading) .onErrorReturn { GetPrescriptionsStatus.Error(it) } } } private fun getPrescriptions(request: GetPrescriptionsRequest): Single<List<Prescription >> { return userAccountsRepository.getLoggedInUser() .flatMapObservable { prescriptionsRepository.getPrescriptions(it) } } }
  20. After class GetPrescriptionsUseCase ( private val prescriptionsRepository: PrescriptionsRepository, private val

    userAccountsRepository: UserAccountsRepository ) { suspend operator fun invoke(): List<Prescription> { val userAccount = userAccountsRepository.getLoggedInUser() return prescriptionsRepository.getPrescriptions(userAccount) } }
  21. suspend fun <T : Any> Call<T>.asSuspend(): T { ... //

    Coroutine directly returning the result } fun <T : Any> Call<T>.asSingle(): Single<T> { ... // Rx single emitting the result } fun <T : Any> Call<T>.asStatusObservable(): Observable<Status<T >> { ... // Rx stream with updates on the status of the execution }