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

Extending Retrofit for Fun and Profit

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers. →
Avatar for Matt Clarke Matt Clarke
September 13, 2017

Extending Retrofit for Fun and Profit

This talk covers a real-world example of how Retrofit can be extended to solve app-wide problems by following the principle of Convention over Configuration.

Originally presented at the Auckland GDG (Google Developers Group).

Avatar for Matt Clarke

Matt Clarke

September 13, 2017

Other Decks in Programming

Transcript

  1. // Retrofit turns your HTTP API into a Java (or

    Kotlin) interface public interface GitHubService { @GET("users/{user}/repos") Call<List<Repo>> listRepos(@Path("user") String user); }
  2. Call<List<Repo>> repos = service.listRepos("octocat"); // asynchronous request repos.enqueue(new Callback<List<Repo>>() {

    @Override public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) { // do stuff with response... } @Override public void onFailure(Call<List<Repo>> call, Throwable t) { // uh oh! } });
  3. Call<List<Repo>> repos = service.listRepos("octocat"); // asynchronous request repos.enqueue(new Callback<List<Repo>>() {

    @Override public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) { // do stuff with response... } @Override public void onFailure(Call<List<Repo>> call, Throwable t) { // uh oh! } });
  4. // Retrofit turns your HTTP API into a Java (or

    Kotlin) interface public interface GitHubService { @GET("users/{user}/repos") Call<List<Repo>> listRepos(@Path("user") String user); }
  5. // Retrofit turns your HTTP API into a Java (or

    Kotlin) interface public interface GitHubService { @GET("users/{user}/repos") Observable<List<Repo>> listRepos(@Path("user") String user); }
  6. WTF is a Call Adapter? • Knows how to convert

    Call<R> to some other type T • E.g. the RxJavaCallAdapter can convert a Call<List<Repo>> into Observable<List<Repo>>
  7. WTF is a Call Adapter? public interface CallAdapter<R, T> {

    T adapt(Call<R> call); Type responseType(); }
  8. WTF is a Call Adapter? • You also need to

    create a CallAdapter.Factory to give to the Retrofit.Builder • Factories create CallAdapters for a given return type in the Service interface (e.g. Observable)
  9. abstract class Factory { /** * Returns a call adapter

    for interface methods that * return returnType, or null if it cannot be handled by * this factory. */ public abstract @Nullable CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit); // .. }
  10. ‘Engage Mobile’ Patient App • Allows patients to access different

    types of healthcare data • E.g. their medications and lab results
  11. App Architecture Data Domain Presentation View Activities, Fragments, RecyclerViewAdapters Presenters,

    View Model Mappers Use Cases, Domain Models Repositories, Caching, Network Adapters e.g. Retrofit Interfaces
  12. Feature-specific Retrofit Interfaces • Separate REST Interface per feature ◦

    MedicationStatementsApiClient, LabResultsApiClient, ... • Singleton Retrofit instance
  13. Problem: Network Error handling • Only 3 types of “network

    error” should be shown to the user: ◦ Internet Connection error ◦ Server error ◦ Unknown error (everything else) • The RxJavaCallAdapter just forwards low-level network Exceptions via onError()!
  14. Problem: Network Error handling medsObservable.subscribe(object : Observer<Medication> { override fun

    onNext(medication: Medication) { // add to meds list... } override fun onCompleted() {} override fun onError(e: Throwable) { when (e) { is SocketTimeoutException -> showNoConnectionToServerError() is ConnectException -> showNoConnectionToServerError() is UnknownHostException -> showNoNetworkConnectionError() is HttpException -> makeSenseOfHttpResponseCode(e) else -> showGenericNetworkError() } } })
  15. Network Error Handling sealed class NetworkError(cause: Throwable?) : Throwable(cause) {

    class NetworkUnreachableError(cause: Throwable? = null) : NetworkError(cause) class ServerUnreachableError(cause: Throwable? = null) : NetworkError(cause) class GenericNetworkError(cause: Throwable? = null) : NetworkError(cause) }
  16. Network Error Handling fun mapNetworkError(input: Throwable): Throwable { if (!networkAccessAvailable())

    return NetworkError.NetworkUnreachableError(input) return when (input) { is SocketTimeoutException -> NetworkError.ServerUnreachableError(cause = input) is ConnectException -> NetworkError.ServerUnreachableError(cause = input) is UnknownHostException -> NetworkError.NetworkUnreachableError(cause = input) is HttpException -> mapHttpException(input) else -> input } }
  17. WTF is a Call Adapter? public interface CallAdapter<R, T> {

    T adapt(Call<R> call); Type responseType(); }
  18. class RxCallAdapterWrapper(val wrappedAdapter: CallAdapter<*>, val errorMapper: (Throwable) -> Throwable) :

    CallAdapter<Observable<*>> { override fun responseType(): Type = wrappedAdapter.responseType() override fun <R> adapt(call: Call<R>): Observable<*> { val observable = wrappedAdapter.adapt(call) as Observable<*> return observable.onErrorResumeNext { throwable -> Observable.error(errorMapper(throwable)) } } }
  19. abstract class Factory { /** * Returns a call adapter

    for interface methods that * return returnType, or null if it cannot be handled by * this factory. */ public abstract @Nullable CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit); // .. }
  20. class RxErrorMappingCallAdapterFactory private constructor( val errorMapper: (Throwable) -> Throwable) :

    CallAdapter.Factory() { companion object { fun create(errorMapper: (Throwable) -> Throwable) = RxErrorMappingCallAdapterFactory(errorMapper) } private val originalFactory = RxJavaCallAdapterFactory.create() override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<Observable<*>> = RxCallAdapterWrapper( originalFactory.get(returnType, annotations, retrofit), errorMapper ) }
  21. medsObservable.subscribe(object : Observer<Medication> { override fun onNext(medication: Medication) { //

    add to meds list... } override fun onCompleted() {} override fun onError(e: Throwable) { when (e) { is SocketTimeoutException -> showNoConnectionToServerError() is ConnectException -> showNoConnectionToServerError() is UnknownHostException -> showNoNetworkConnectionError() is HttpException -> makeSenseOfHttpResponseCode(e) else -> showGenericNetworkError() } } })
  22. medsObservable.subscribe(object : Observer<Medication> { override fun onNext(medication: Medication) { //

    add to meds list... } override fun onCompleted() {} override fun onError(e: Throwable) { when (e) { is NetworkError.NetworkUnreachableError -> showNetworkErrorView() is NetworkError.ServerUnreachableError -> showServerErrorView() else -> showUnknownErrorView() } } })
  23. Conclusion • Having to remember = red flag • Convention

    over configuration • Good libraries are often extensible • Don’t be afraid of checking out the source of your favourite open source libs