Retrofit is a library, which is useful in most apps one can think of. If the app needs a backend, you can – and should – use Retrofit for RESTful services. It’s remarkably easy to setup – you’ll have networking up and running in no time, automatically getting the responses parsed into your model objects.
Often you also want to persist some of the data that is fetched from the backend. Realm is very popular, because you can work with “normal” objects, instead of having to use SQLite (even though many ORM libs exist). Also, RealmObjects are live objects, so you always get access to the newest data.
Another problem which often arises is that you may want to save only some of the fetched objects in your database, but you still need to persist the rest when the Activity gets killed by using onSaveInstanceState() (e.g. a list with locally cached bookmarked/favorited items). Parceler provides an easy solution for this problem, since it makes your objects Parcelable via an annotation processor.
Together these three libraries make working with data from a backend quick and easy. This blog post guides you through the setup process.
Retrofit
For this article, we assume that your backend exposes a RESTful service and the responses are in JSON format. For JSON parsing, we will be using Gson. Add the following dependencies to your build.gradle:
compile 'com.google.code.gson:gson:2.8.0' compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' compile 'com.squareup.okhttp3:okhttp:3.6.0' compile 'com.squareup.okhttp3:logging-interceptor:3.6.0'
The first step is defining your model classes. For now, just create simple classes with public fields.
public class Country { public String alpha2Code; public String name; public String region; public List<String> languages; ... }
Now define the calls which will be used. With Retrofit, this is accomplished by creating an interface and declaring a method for each call. The return type for each method is Call<ModelClass>. Of course, a return type of Call<List<ModelClass>> is also possible, if your call returns an array. The parameters for a call can be annotated with @Query, @Path or @Body, depending on your needs. Finally, annotate the call with the HTTP request method and URL (relative to your service’s base URL), e.g. @GET(“relative/path”). Here is an example:
public interface ICountryApi { @GET("region/{region}") Call<List<Country>> getAllCountries(@Path("region") String region); }
Now it’s time to make some Retrofit magic happen. Retrofit does all the heavy lifting for you and creates an object which implements your interface. This object should be reused, e.g. by using dependency injection or a singleton.
Gson gson = new GsonBuilder() .create(); OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); if(BuildConfig.DEBUG) { // enable logging for debug builds HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); httpClientBuilder.addInterceptor(loggingInterceptor); } ICountryApi countryApi = new Retrofit.Builder() .baseUrl("https://restcountries.eu/rest/v1/") .addConverterFactory(GsonConverterFactory.create(gson)) .callFactory(httpClientBuilder.build()) .build().create(ICountryApi.class);
Now you can make backend calls with your service instance. Calls can be executed synchronously via Call.execute(), which directly returns a Response<ModelClass> and throws exceptions on failure, or asynchronously via Call.enqueue(), which is callback based. In most cases, you’ll want to use the asynchronous enqueue().
countryApi.getAllCountries().enqueue(new Callback<List<Country>>() { @Override public void onResponse(Call<List<Country>> call, Response<List<Country>> response) { if(response.isSuccessful()) { List<Country> countries = response.body(); } else { // handle error } } @Override public void onFailure(Call<List<Country>> call, Throwable t) { // handle error } });
What seems strange at first is that onResponse() is also called, if the response was not successful. However, successful here just means that the response has a HTTP status in the 200 range. You can still access the body of the response if it was not successful, e.g. to read an error message or error code from the server.
Adding RxJava support
Backend calls can also be declared with a RxJava Observable<ModelClass> return type, gaining all the benefits of Observables, e.g. easy call chaining. To enable this, add another dependency and a call adapter factory when building your Retrofit service:
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' new Retrofit.Builder() .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) ...
Realm
To include Realm in your app, you need to add a buildscript dependency classpath “io.realm:realm-gradle-plugin:2.3.0” and apply a plugin in your app’s build.gradle apply plugin: ‘realm-android’. Keep in mind that adding Realm adds about 3MB to your APK, because the native libs which are used by Realm have to be included for multiple CPU architectures. However, you can reduce this overhead by using APK Splits.
RealmObjects are accessed by opening and querying a Realm. A Realm is always confined to a thread, so you need to open a new Realm if you want to access RealmObjects in a different thread. However, since RealmObjects are live objects, you can always access the newest data, but if your thread is not a looper thread, you manually need to call refresh() on your Realm instance before you see any changes.
Since the latest Realm release, it’s also possible to implement interfaces and add custom setters/getters, methods or just use public fields. However, your objects still need to extend RealmObject. You can also add a primary key or define indices via annotations. See the Realm docs for more information.
public class Country extends RealmObject { @PrimaryKey public String alpha2Code; public String name; public String region; public RealmList<RealmString> languages; ... }
Another (annoying) restriction of Realm is that a RealmList currently can only contain RealmObjects. Therefore you need to wrap primitives and Strings like this:
public class RealmString extends RealmObject { public String value; }
However, now you have a problem with Retrofit and Gson. Gson doesn’t know that RealmString ist just a wrapper class … yet. So let’s implement a TypeAdapter to make it work:
public class RealmStringListTypeAdapter extends TypeAdapter<RealmList<RealmString>> { public static final TypeAdapter<RealmList<RealmString>> INSTANCE = new RealmStringListTypeAdapter().nullSafe(); private RealmStringListTypeAdapter() { } @Override public void write(JsonWriter out, RealmList<RealmString> src) throws IOException { out.beginArray(); for(RealmString realmString : src) { out.value(realmString.value); } out.endArray(); } @Override public RealmList<RealmString> read(JsonReader in) throws IOException { RealmList<RealmString> realmStrings = new RealmList<>(); in.beginArray(); while (in.hasNext()) { if(in.peek() == JsonToken.NULL) { in.nextNull(); } else { RealmString realmString = new RealmString(); realmString.value = in.nextString(); realmStrings.add(realmString); } } in.endArray(); return realmStrings; } }
You have to register this TypeAdapter when building your Gson instance:
new GsonBuilder() .registerTypeAdapter(new TypeToken<RealmList<RealmString>>(){}.getType(), RealmStringListTypeAdapter.INSTANCE) ...
Now your RealmObjects can be persisted, after fetching them from the backend. When using Realm, writing operations need to be wrapped inside a Realm transaction:
Realm realm = Realm.getDefaultInstance(); List<Country> countries = response.body(); realm.beginTransaction(); realm.copyToRealmOrUpdate(countries); realm.commitTransaction();
After saving the objects, they can be accessed by using Realm’s query methods – but remember, live RealmObjects are thread confined.
List<Country> allSavedCountries = realm.allObjects(Country.class); Country specificCountry = realm.where(Country.class).equalTo("alpha2Code", "AT").findFirst();
With this approach, it’s quite easy to mix up live RealmObjects and detached objects of the same class, e.g. in a List. Since writes on a live object change the persisted data, you have to be careful (however, writes are only allowed inside of a transaction anyway). If you want to get a detached copy, you can use realm.copyFromRealm(liveRealmObject);, which also works for lists. You can use realmObject.isValid() to check whether a RealmObject is a live instance or a detached copy.
Parceler
To pass around data or save state in Android, the objects need to implement Serializable or Parcelable. Parcelable is considered faster, because there is no reflection overhead (and less allocations), therefore being better suited for mobile apps. However, implementing Parcelable requires quite some effort. While there is automatic code generation in Android Studio, this step needs to be done every time your class changes. Annoying. Parceler to the rescue!
Since Parceler uses an annotation processor, make sure you use version 2.2.0 of the Android Gradle plugin, or later. Now you can include the Parceler library in your dependencies:
compile 'org.parceler:parceler-api:1.1.6' annotationProcessor 'org.parceler:parceler:1.1.6'
With Parceler, you can annotate classes with @Parcel and the annotation processor automatically creates a Parcelable implementation for you. Awesome! To make this work with RealmObjects you need some additional configuration:
@Parcel(implementations = { CountryRealmProxy.class }, value = Parcel.Serialization.FIELD, analyze = { Country.class }) public class Country extends RealmObject { @PrimaryKey public String alpha2Code; public String name; public String region; @ParcelPropertyConverter(RealmListParcelConverter.class) public RealmList<RealmString> languages; ... }
By setting the analyze and implementations attributes, you tell Parceler that it should also accept CountryRealmProxy objects – these are the proxy classes used by Realm. If the RealmProxy class for your RealmObject is not available, try to build your project, as it is generated in this step.
Parceler doesn’t know how to handle a RealmList in the default setup. You have to provide a custom ParcelConverter to make it work. I wrote a RealmListParcelConverter which works with any RealmList, as long as the items are also annotated with @Parcel, i.e. they also use Parceler.
public class RealmListParcelConverter implements TypeRangeParcelConverter<RealmList<? extends RealmObject>, RealmList<? extends RealmObject>> { private static final int NULL = -1; @Override public void toParcel(RealmList<? extends RealmObject> input, Parcel parcel) { parcel.writeInt(input == null ? NULL : input.size()); if (input != null) { for (RealmObject item : input) { parcel.writeParcelable(Parcels.wrap(item), 0); } } } @Override public RealmList fromParcel(Parcel parcel) { int size = parcel.readInt(); RealmList list = new RealmList(); for (int i=0; i<size; i++) { Parcelable parcelable = parcel.readParcelable(getClass().getClassLoader()); list.add((RealmObject) Parcels.unwrap(parcelable)); } return list; } }
To use your @Parcel annotated objects as a Parcelable, you have to wrap them by Parcels.wrap(object). To get your original object from a Parcelable call Parcels.unwrap(parcelable). These methods also work with Lists of @Parcel annotated objects. Easy.
Conclusion
Setting up Retrofit with Realm and Parceler requires some initial effort, however using this setup is easy and painless:
List<Country> countries; public void getCountries() { countryApi.getAllCountries().enqueue(new Callback<List<Country>>() { @Override public void onResponse(Call<List<Country>> call, Response<List<Country>> response) { if(response.isSuccessful()) { countries = response.body(); realm.beginTransaction(); // Copy the objects to realm. The list still contains detached objects. // If you want to use the live objects, you have to use the return value // of this call. realm.copyToRealmOrUpdate(countries); realm.commitTransaction(); } else { // handle error } } @Override public void onFailure(Call<List<Country>> call, Throwable t) { // handle error } }); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable("countries", Parcels.wrap(countries)); } @Override protected void onCreate(Bundle savedInstanceState) { ... if(savedInstanceState != null) { countries = Parcels.unwrap(savedInstanceState.getParcelable("countries")); } }
An example project using the approach outlined in this article is available on GitHub.
What are your experiences with using these libs? Do you prefer another approach? I’d love to hear about it in the comments.
Cool! Have you tried AutoValue-Parcel extension? That makes all the generated code up front. Haven’t tried it yet but am going to. Also, AutoValue generates equalsTo, hashCode and toString methods 🙂 https://github.com/rharter/auto-value-parcel
https://github.com/google/auto/tree/master/value
FYI, Android Studio will generate equalsTo, hashCode, and toString for you. In your object, just right-click where you want to put the methods, select “Generate…”, then select what you want to generate. Bam – done.
This feature is amazing — the equalsTo and hashCode generation asks you which fields you want to include in the calculation. Android Studio gets better every day :).
Done until you want to change something 😉
You can use Lombok annotation processor now so that you don’t have to generate any additional code.
Thanks for sharing the article! You mentioned RxJava support for Retrofit, could you elaborate a bit how will it be used with Realm?
I was doing the exactly same thing! You save me a ton of time!! Awesome article!
Thanks for your nice article, but I got problem when I’m trying to implement Parceler, my RealmProxy class for RealmObject is not available even if I try to build my project. Can you help me for my problem?
Thank you for the article. But why don’t you use JSON directly from retrofit and insert it into realm as JSON. So you don’t have to be bothered with conversion from List to RealmList.