Android: Realm + Retrofit 2 + Gson

I have a problem when using Retrofit + Gson and Realm. I know that there is an issue with the combination of these 3 libraries. Some answers suggest that setting an ExclusionStrategy for Gson can solve this issue, and I tried it but it didn't work.

My code looks like:

public class ObjectList {
    public List<AnotherObject> anotherObject;
 }

public class AnotherObject extends RealmObject {
    private String propA;
    public void setPropA(String propA){
       this.setPropA = propA
    }
    public String getPropA(){
       return propA
    }
}

        Gson gson = new GsonBuilder().setExclusionStrategies(new  ExclusionStrategy() {
        @Override
        public boolean shouldSkipField(FieldAttributes f) {
            return f.getDeclaringClass().equals(RealmObject.class);
        }

        @Override
        public boolean shouldSkipClass(Class<?> clazz) {
            return false;
        }
    }).create();

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("http://localhost/api/")
            .addConverterFactory(GsonConverterFactory.create(gson))
            .build();
    ObjectAPI objectAPI = retrofit.create(ObjectAPI.class);
    call.enqueue(new Callback<ObjectList>() {
        @Override
        public void onResponse(Response<ObjectList> response, Retrofit retrofit) {
            objectList = response.body().anotherObject;
            onRefreshComplete();
        }

        @Override
        public void onFailure(Throwable t) {
            Toast.makeText(context, "Connection to server failed, please check your connection", Toast.LENGTH_LONG).show();
        }
    });

With the current code, I'm still getting the memory leak. Is there any suggestion for this code?

My json structure looks like:

{"anotherObject":[{"propA": "someValue"}]}

Solution 1:

Why writing all these custom serializers when you can make Gson and Realm work together with just ONE LINE OF CODE?

TL;DR.

You can simply solve this by passing unmanaged RealmObjects to your Retrofit calls.

If you don't want to go through all this answer, then skip to the "Recommended solutions" section posted down below.

Long talk (verbose answer)

This has nothing to do with Retrofit. If you have set Gson to be the data converter to your current Retrofit instance, then you can be sure that it's Gson who's failing.

Suppose we have this Model:

public class Model extends RealmObject {
    @PrimaryKey
    long id;
    boolean happy;

    public Model() {/* Required by both Realm and Gson*/}

    public Model(long id, boolean happy) {
        this.id = id;
        this.happy = happy;
    }

    public long getId() {
        return id;
    }

    public boolean isHappy() {
        return happy;
    }
}

For this code, we'll have no issue:

Model unmanagedModel = new Model(5, true); // unmanagedModel
new Gson().toJson(unmanagedModel);   // {id : 5, happy : true}

But for this one:

Realm realm = /*...*/;
Model managedModel = realm.copyToRealm(unmanagedModel);
new Gson().toJson(managedModel); // {id : 0, happy : false}

// We'll get the samething for this code
Model anotherManagedModel = realm.where(Model.class).equalTo("id",5).findFirst();
new Gson().toJson(anotherManagedModel); // {id : 0, happy : false}

We'll be surprised. We're seeing nulls everywhere!.

Why?

Gson fails serializing a RealmObject only if it's a managed one. Which means that there's currently an opened Realm instance making sure this RealmObject is reflecting what is currently held in the persistence layer (the Realm database).

The reason why this is happening is due to the conflicting nature of how both Gson and Realm work. Quoting Zhuinden on why Gson sees null everywhere:

... that's because GSON tries to read the fields of the Realm object via reflection, but to obtain the values, you need to use accessor methods - which are automatically applied to all field access in the code via the Realm-transformer, but reflection still sees nulls everywhere...

Christian Melchior proposes a workaround to this conflict by writing a custom JsonSerializers to every created Model. This is the workaround you have used, but I would NOT recommend it. As you have realized, it requires writing a lot of code which is error prone and the worst of all, kills what Gson is about (which is making our life less painful).

Recommended solutions

If we can somehow make sure the realmObject we pass to Gson is not a managed one, we'll avoid this conflict.

Solution 1

Get a copy in memory of the managed RealmObject and pass it to Gson

new Gson().toJson(realm.copyFromRealm(managedModel));

Solution 2

(Wrapping the 1st solution). If the 1st solution is too verbose for you, make your models look like this one:

public class Model extends RealmObject {
    @PrimaryKey
    long id;
    boolean happy;
    
    // Some methods ...

    public Model toUnmanaged() {
        return isManaged() ? getRealm().copyFromRealm(this) : this;
    }
}

And then, you can do something like this:

// always convert toUnmanaged when serializing
new Gson().toJson(model.toUnmanaged());

Solution 3

This one is NOT very practical but is worth mentioning. You can go with deep-cloning your models (taken from here).

1 - Create a generic interface CloneableRealmObject:

interface CloneableRealmObject<T> {
    T cloneRealmObject();
}

2 - Make your realmObjetcs implement the above interface like so:

public class Model extends RealmObject implements CloneableRealmObject<Model> {
    @PrimaryKey
    long id;

    public Model() {
        // Empty constructor required by Realm.
    }

    @Override
    public Model cloneRealmObject() {
        Model clone = new Model();
        clone.id = this.id;
        return clone;
    }
}

3 - Clone the object before passing to your Retrofit calls.

new Gson().toJson(model.cloneRealmObject());

In a recent post

I gave an answer explaining why we're getting this weird serialized output when using managed realmObjects. I recommend you to take a look at it.

Bonus

You might also want to check RealmFieldNamesHelper, a library made by Christian Melchior "to make Realm queries more type safe".

Solution 2:

I too faced the similar issue. This is because your request format is wrong. In my case, I am trying to send a Realm object by getting it from local SQLite DB instead of Java object. Retrofit converts only Java object to JSON but not Realm object. Please make sure you are sending a right JSON as a request when using Retrofit.

Then I replaced this:

List<MyRealmModel> objectsToSync = mRealm.where(MyRealmModel.class).findAll();

To:

List<MyRealmModel> objectsToSend = mRealm.copyFromRealm(objectsToSync);