Retrofit: Obliterating HTTP Boilerplate (Montreal)

Retrofit: Obliterating HTTP Boilerplate (Montreal)

Presented 4/10/2015 at Droidcon Montreal

E26f3fe144ed1fa6def3dc7b2b29c8d1?s=128

Jacob Tabak

April 10, 2015
Tweet

Transcript

  1. RETROFIT OBLITERATING HTTP BOILERPLATE JACOB TABAK @JACOBTABAK

  2. REST API → JAVA INTERFACE interface UserService {
 @GET("/users/me") UserProfile

    getProfile();
 } BASIC USAGE: UserProfile profile = userService.getProfile(); * some assembly required
  3. ACTUALLY DO? • Parses the ANNOTATIONS, PARAMETERS, and RETURN TYPE

    of your interface methods
 • Uses this metadata to build a REQUEST
 • Fulfills the request by delegating to a CLIENT and a CONVERTER WHAT DOES RETROFIT
  4. CONFIGURING RETROFIT WITH REST ADAPTER new RestAdapter.Builder() .setEndpoint(“https://api.github.com") .setClient(new OkClient())

    .setConverter(new GsonConverter(new Gson())) .setLog(new AndroidLog(“retrofit")) .setLogLevel(RestAdapter.LogLevel.FULL) .setRequestInterceptor(new MyRequestInterceptor()) .setErrorHandler(new MyErrorHandler()) .build(); Only .setEndpoint() is required
  5. HOW IT WORKS (Implementation Details) GitHubService service = restAdapter.create(GitHubService.class); •

    The service is a proxy for your interface • The proxy dispatches method invocations to an InvocationHandler • The InvocationHandler uses reflection to create and cache a RestMethodInfo for each method • The RestMethodInfo is used to build a Request • The Client executes the request • The Converter deserializes the response body and returns an object of the correct type.
  6. REST ADAPTER 
 RECIPES

  7. LOGGING • Default log dumps to LogCat • Check out

    Jake Wharton’s “Timber” library • Example: Log to Timber restAdapter .setLogger(new RestAdapter.Log() {
 @Override
 public void log(String message) {
 Timber.v(message);
 }
 }) .setLogLevel(LogLevel.FULL);
  8. ERROR HANDLING • Set a global ErrorHandler in your RestAdapter

    • Throw typed exceptions when known errors occur • Example: log user out when receiving HTTP 401 restAdapter.setErrorHandler(new ErrorHandler() {
 @Override public Throwable handleError(RetrofitError cause) {
 if (cause.getResponse() != null && cause.getResponse().getStatus() == 401) {
 session.close();
 return new UnauthorizedException(cause);
 } 
 return cause;
 }
 })
  9. INTERCEPTING REQUESTS • Add headers, query parameters, and/or path parameters

    to every request • Great for authentication tokens or metadata like app version, platform, accept language, etc. • Interceptors in 1.0 are limited and can’t access the request body. • Example: add authentication token header restAdapter.setRequestInterceptor(new RequestInterceptor() {
 @Override public void intercept(RequestFacade request) {
 request.addHeader("auth-token", "supersecret");
 }
 })
  10. CUSTOMIZING THE CLIENT • Retrofit delegates to the client to

    make requests • Client is responsible for caching and cookie mgmt • Use OkHttp - you won’t have a choice later! • OkHttp interceptors can overwrite headers
  11. THREE WAYS TO MAKE A REQUEST

  12. try { UserProfile profile = userService.getProfile(); } catch (RetrofitError e)

    {
 // Error handling code
 } SYNCHRONOUSLY interface UserService {
 @GET("/users/me") UserProfile getProfile();
 }
  13. userService.getProfile( new Callback<UserProfile>() {
 @Override
 public void success(UserProfile profile, Response

    response) { }
 
 @Override
 public void failure(RetrofitError error) { }
 } ); ASYNCHRONOUSLY interface UserService {
 @GET("/users/me") void getProfile(Callback<UserProfile> profile);
 }
  14. userService.getProfile().subscribe( new EndlessObserver() { @Override
 public void onNext(UserProfile profile) {

    } ! @Override
 public void onError(Throwable error) { } }
 ); WITH RXJAVA interface UserService {
 @GET("/users/me") Observable<UserProfile> getProfile();
 }
  15. WHY RXJAVA? • Easily compose dependent requests into one •

    Wait for multiple requests to complete before executing a callback • Only requires one error handler when composing requests or stream operations • Easily switch between threads depending on circumstance • Control timing of response callbacks when testing
  16. HOW TO DECLARE A SERVICE INTERFACE

  17. Use @GET, @POST, @PUT, @DELETE, @PATCH, or @HEAD interface UserService

    { @GET("/users/12345")
 Response getUser();
 
 @DELETE("/users/12345")
 Response deleteUser(); } HTTP METHOD START WITH THE
  18. METHOD PARAMETERS THEN MOVE ON TO YOUR @GET("/users/{user_id}") 
 UserProfile

    getProfile(@Path("user_id") String userId); @GET("/users") 
 UserProfile getProfile(@Query("user_id") String userId); @GET("/secret") 
 UserProfile getProfile(@Header("authorization") String password); ---> HTTP GET https://api.tabak.me/users/12345 ---> HTTP GET https://api.tabak.me/users?user_id=12345 ---> HTTP GET https://api.tabak.me/secret authorization: Basic aHR0cHdhdGNoOmY=
  19. BODY ENCODING @FormUrlEncoded
 @POST("/update_profile")
 Response updateProfile(@Field("name") String name); @Multipart
 @POST("/upload_photo")


    Response uploadPhoto(@Part("photo") TypedFile picture,
 @Part("caption") String caption); SET YOUR
  20. STREAMING RESPONSES @Streaming
 @GET("/download_photo")
 Response downloadPhoto(); • Bypasses the converter

    • Return value must be of the Response type • Allows raw access to a response bytes • Can be used to download a files
  21. MODELING Translating API responses to POJOs (Plain Old Java Objects)

  22. THE CONVERTER public class UserProfile {
 int id;
 String name;

    } {
 "id": 1,
 "name": "Jacob"
 } Converter JSON/XML Model
 (POJO)
  23. • GSON: Moderate footprint, moderate performance. Default converter for Retrofit

    1.x. • Jackson: Larger footprint, better performance for some use cases • Moshi: Unreleased, will be the default converter, leverages Okio The Client streams bytes to the Converter as they arrive so converter typically won’t be your choke point. CONVERTER CHOOSING A
  24. TIPS & TRICKS MODELING • Use package private visibility on

    fields with public getters and setters when applicable • Only use constructors when necessary • Transient fields will not be serialized • Use @NonNull and @Nullable everywhere (add support- annotations dependency) public class UserProfile {
 int id;
 @Nullable String name;
 @NonNull List<UserProfile> friends;
 
 public int getId() { 
 return id; 
 }
 
 @Nullable 
 public String getName() { 
 return name; 
 }
 
 @NonNull 
 public List<UserProfile> getFriends() { 
 return friends; 
 }
 }
  25. JSON ADAPTERS Basic types are converted automatically: primitives, strings, lists,

    arrays, maps, dictionaries ! Other classes need a JsonAdapter
  26. DateTime TypeAdapter // NULL CHECKS OMITTED public class DateTimeTypeAdapter extends

    TypeAdapter<DateTime> {
 @Override
 public DateTime read(JsonReader in) throws IOException {
 long seconds = in.nextLong();
 return new DateTime(seconds * 1000);
 }
 
 @Override
 public void write(JsonWriter out, DateTime value) throws IOException {
 out.value(value.getMillis() / 1000);
 }
 } Converts an epoch timestamp to a DateTime object
  27. Interfaces & Abstract Classes

  28. Interfaces & Abstract Classes [
 {
 "type": "fbphoto",
 "data": {


    "url": “https://img.ur/fb.jpg”,
 "caption": "Lovely day",
 "likes": [],
 "comments": []
 }
 },
 {
 "type": "tweet",
 "data": {
 "url": “https://img.ur/tweet.jpg”,
 "caption": "Great Scott!",
 "favorites": [],
 "retweets": []
 }
 }
 ] public enum PhotoType {
 fbphoto(FacebookPhoto.class),
 tweet(TwitterPhoto.class);
 
 public final Type type;
 
 PhotoType(Class<? extends Photo> type) {
 this.type = type;
 }
 }
  29. Interfaces & Abstract Classes public class PhotoTypeAdapter implements
 JsonDeserializer<Photo>, JsonSerializer<Photo>

    {
 
 @Override public Photo deserialize(
 JsonElement json, Type typeOfT, JsonDeserializationContext context) {
 JsonObject container = json.getAsJsonObject();
 String type = container.get("type").getAsString();
 JsonElement data = container.get("data");
 Type photoType = PhotoType.valueOf(type).type;
 return context.deserialize(data, photoType);
 }
 
 @Override public JsonElement serialize(
 Photo src, Type typeOfSrc, JsonSerializationContext context) {
 JsonObject container = new JsonObject();
 JsonPrimitive type = new JsonPrimitive(getPhotoType(typeOfSrc));
 JsonElement data = context.serialize(src);
 container.add("type", type);
 container.add("data", data);
 return container;
 }
 }
  30. TESTING

  31. RETROFIT-MOCK • Used to test the rest of your app

    (not your Retrofit implementation) • Define a class that implements service interface • Simulate network latency, variance, and error rate • Does not test your Converter or Client
  32. RETROFIT-MOCK MockRestAdapter mockRestAdapter = MockRestAdapter.from(restAdapter); mockRestAdapter.setDelay(2000);
 mockRestAdapter.setErrorPercentage(10);
 mockRestAdapter.setVariancePercentage(25);
 UserService mockService

    = mockRestAdapter.create(UserService.class, new MockUserService()); public class MockUserService implements UserService {
 @Override public UserProfile getProfile(int userId) {
 UserProfile userProfile = new UserProfile();
 userProfile.id = userId;
 userProfile.name = "Jacob";
 return userProfile;
 }
 }
  33. MOCK WEBSERVER • Contrib library for OkHTTP: com.squareup.okhttp:mockwebserver • Copy/paste

    real responses into your tests • Ideal for unit testing • Useful for testing Converter (JsonAdapters), Client, RequestInterceptor, ErrorHandler
  34. MOCK WEBSERVER MockWebServer webServer = new MockWebServer();
 webServer.start(); UserService service

    = new RestAdapter.Builder()
 .setEndpoint(webServer.getUrl("").toString())
 .build()
 .create(UserService.class); webServer.enqueue(
 new MockResponse().setBody("{ \"id\": 1, \"name\": \"Jacob\" }”)); UserProfile profile = service.getProfile(1);
 Assert.assertEquals(profile.getId(), 1);
 Assert.assertEquals(profile.getName(), "Jacob");
  35. TESTING WITH ESPRESSO † DAGGER “The centerpiece of Espresso is

    its ability to seamlessly synchronize all test operations with the application under test. By default, Espresso waits for UI events in the current message queue to process and default AsyncTasks to complete before it moves on to the next test operation.”
  36. TESTING WITH ESPRESSO † DAGGER // in TestApiModule.java @Provides @Singleton


    Executor provideHttpExecutor() {
 return AsyncTask.THREAD_POOL_EXECUTOR;
 } ! // in ApiModule.java @Provides @Singleton
 RestAdapter provideRestAdapter(Endpoint endpoint, Converter converter, Executor httpExecutor,
 Executor callbackExecutor, RequestInterceptor requestInterceptor, RestAdapter.LogLevel logLevel, 
 RestAdapter.Log log, ErrorHandler errorHandler, OkHttpClient okHttpClient) {
 return new RestAdapter.Builder()
 .setClient(new OkClient(okHttpClient))
 .setEndpoint(endpoint)
 .setErrorHandler(errorHandler)
 .setRequestInterceptor(requestInterceptor)
 .setConverter(converter)
 .setLog(log)
 .setLogLevel(logLevel)
 .setExecutors(httpExecutor, callbackExecutor)
 .build();
 }
  37. TESTING TIMINGS WITH MOCKITO + RX PUBLISHSUBJECT // in TestModule.java

    @Provides @Singleton
 UserService provideMockUserService() {
 return mock(UserService.class);
 } @Inject UserService mockUserService;
 
 public void testRequestTiming() {
 PublishSubject<UserProfile> mockObservable = PublishSubject.create();
 when(mockUserService.getUserProfile()).thenReturn(mockObservable);
 onView(withId(R.id.load_profile_button)).perform(click()); ! // Profile is loading, profile should not be visible
 onView(withId(R.id.profile_view)).check(doesNotExist());
 mockObservable.onNext(new UserProfile()); ! // Profile has finished loading, profile view should be visible
 onView(withId(R.id.profile_view)).check(matches(isDisplayed()));
 }
  38. 2.0: COMING SOON

  39. 2.0: COMING SOON • No more client abstraction • Unified

    API for sync/async calls • Response<T> return type? • Improved RX support - onError
  40. Thanks! JACOB TABAK @JACOBTABAK