Android: Realm + Retrofit 2 + Gson - java

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"}]}

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.
MyModel model = realm.where(MyModel.class).findFirst();
MyModel unmanagedModel = realm.copyFromRealm(model);
// then pass unmanagedModel 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".

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);

Related

java.lang.UnsupportedOperationException: This feature is available only when the element type is implementing RealmModel

In my app, I'm saving an Arraylist to realmlist and that list is being saved to realm database so now at runtime when I try to fetch result with some search query it gives me java.lang.UnsupportedOperationException: This feature is available only when the element type is implementing RealmModel.
I can't seem to understand what is wrong here. Any help will be appreciated!
Here is my realm class:
public class Vendordb extends RealmObject {
public RealmList<String> getVendor() {
return vendor;
}
public void setVendor(RealmList<String> vendor) {
this.vendor = vendor;
}
RealmList<String> vendor = new RealmList<>();
}
This is the code for create realmlist:
final List<String> vendors = macvendorDatabaseAccess.getvendors();
final RealmList<String> vend = new RealmList<>();
vend.addAll( vendors );
macvendorDatabaseAccess.close();
realm.executeTransaction( new Realm.Transaction() {
#Override
public void execute(Realm realm) {
Vendordb vendordb = realm.createObject( Vendordb.class );
vendordb.setVendor(vend);
}
} );
//At this piece of code i'm getting error:
Vendordb vendordb = realm.where(Vendordb.class).findFirst();
RealmList<String> vendor = vendordb.getVendor();
RealmResults<String> filteredMembers = vendor.where().equalTo("mac", identifier).findAll();
if (!filteredMembers.isEmpty()) {
holder.vendor.setText( filteredMembers.get( 0 ).toString() );
}
Your problem is in setVendor, your use of an unmanaged RealmList outside of an object, and using a query on a non-Realm object (in this case a primitive String).
In setVendor, you cannot reassign the value of the list at this time; the list already exists, so you have to modify its contents.
RealmList should only be used within an object where it is managed. You shouldn't try and use them outside of the object.
Although it's admirable to try and encapsulate the RealmList in the object and provide accessors, it doesn't actually help here; you'll notice the Realm examples always use public members. You can make them private if you wish, but your accessors should then hide the implementation and dependency on Realm and provide you with the necessary operations your application layer needs.
So, delete the getVendor and setVendor methods from your model.
Next, add methods to do your dirty work. E.g.
public class Vendordb extends RealmObject {
RealmList<String> vendor = new RealmList<>();
public void addVendors(List<String> vendors)
{
this.vendor.addAll(vendors);
}
}
Your 'create' section then becomes:
final List<String> vendors = macvendorDatabaseAccess.getvendors();
macvendorDatabaseAccess.close();
realm.executeTransaction( new Realm.Transaction() {
#Override
public void execute(Realm realm) {
Vendordb vendordb = realm.createObject( Vendordb.class );
vendordb.addVendors(vendors);
}
} );
I'm not quite sure what you're trying to do in the final 'search' part. You have a list of strings but are performing a 'where' on a primitive type - this is probably what is generating your runtime error. Think about what you want to pull out of the list of strings, and then add a similar method to find it for you (maybe just a call to contains).

Only valid managed objects can be copied from Realm

I have one-to-many relationship in Realm
public class BayEntity extends RealmObject implements RealmModel {
#PrimaryKey
private int id;
private String title;
}
public class TermEntity extends RealmObject implements RealmModel {
#PrimaryKey
private String termId;
private String name;
private RealmList<BayEntity> bayList;
public void updateBayList(ArrayList<BayEntity> bayList) {
if(CollectionUtility.isEmptyOrNull(this.bayList))
this.bayList = new RealmList();
this.bayList.addAll(bayList);
realm.beginTransaction();
realm.copyToRealm(this.bayList);
realm.insertOrUpdate(this); //update current instance
realm.commitTransaction();
}
public ArrayList<BayEntity> getSimpleList() {
if(CollectionUtility.isEmptyOrNull(bayList))
return new ArrayList<>();
return (ArrayList<BayEntity>) realm.copyFromRealm(bayList);
}
}
I am getting all TermEntity objects containing baylist as:
public RealmResults<TermEntity> getTerms() {
return realm.where(TermEntity.class).findAll();
}
I am able to save and retrieve bayList successfully. When I try to convert it into Arraylist in method getSimpleList(), I am getting exception:
java.lang.IllegalArgumentException: Only valid managed objects can be
copied from Realm.
I can't understand how objects can be unmanaged when they are saved and retrieved from realm. Please correct me. Thanks.
It looks to me as though you have several problems:
Where are you getting the Realm instance that you use in the updateBayList method? Since it is instance local state, it might easily refer to a Realm that is not valid in the current context (time or thread)
The first three lines if ... addAll(bayList); modify Terms RealmList. You can't do that outside of a transaction, if the object is managed. When the object is not managed, you get the error you describe, because you are trying to add managed objects to an unmanaged RealmList
While not a problem, RealmObject already implements RealmModel. Say something once, why say it again?
Edited to add:
In order to add BayEntitys to a TermEntity, first make sure that the TermEntity is a managed object (use copyToRealm, or query for it). Once you do that, the code in updateBayList is very nearly correct. You want something like:
realm.beginTransaction();
if (CollectionUtility.isEmptyOrNull(this.bayList)) {
this.bayList = new RealmList();
}
this.bayList.addAll(bayList);
realm.commitTransaction();

Specifying a class that uses T

I'm using a method that takes a Class<T> as a parameter.
The class I want to pass as a parameter also uses T. It is declared as public class MyObject<T> and has a member declared as public T mMyVar; I then have 2 classes I sometimes use for mMyVar called MyVarObject1 and MyVarObject2.
Example:
private class MyObject<T> {
public T mMyVar;
}
private class MyVarObject1 {
// some variables
}
private class MyVarObject2 {
// some variables
}
Specifically, the method I'm invoking is the JacksonUtil method fromJsonArray.
I'm not sure of the proper syntax here. JacksonUtil needs to know the exact model structure so it can parse the json, but I'm having trouble figuring out the proper syntax for this line:
MyObject<MyVarObject1> result = JacksonUtil.fromJsonArray(jsonStr, MyObject<MyVarObject1>.class);
What I have there doesn't work. My IDE selects the second parameter and says, "Cannot select from parameterized type."
I had a same problem while using with retrofit, This is my solution -
public class ResponseDS<T> {
public int s;
public String e;
public T d;
}
And if you need array of object then,
public class ResponseDSs<T> {
public int s;
public String e;
public T[] d;
}
And below is how I am using it for Retrofit -
Call<ResponseDS<UserDS>> userModelCall = ZivaUtils.getRetrofit().getUser();
I think you have the same problem, hope my solution will help you :)
I do TypedToken from Gson to parse custom objects, I think you can find something similar to use with Jackson, i will edit my answer if i find something later.
You may use TypeToken to load the json string into a custom object.
Gson gson = new Gson();
//This is an example, you probably get this from your server as Json String
MyObject<MyObject1> user = new MyObject<MyObject1>();
String myObjectAsString = gson.toJson(user);
//then parse into your custom object
MyObject other = gson.fromJson(myObjectAsString, new TypeToken<MyObject<MyObject1>>(){}.getType());

GSON not calling my TypeAdapter for a type which is an interface

GSON appears to be doing some kind of trick where it looks at the internal fields of my JavaBeans instead of using the publically-accessible property information. Unfortunately this won't fly for us because our magically-created beans are full of private fields which I don't want it to store off.
#Test
public void testJson() throws Exception
{
Player player = new MagicPlayer(); //BeanUtils.createDefault(Player.class);
player.setName("Alice");
Gson gson = new GsonBuilder()
.registerTypeAdapter(Player.class, new PlayerTypeAdapter())
.create();
System.out.println(gson.toJson(bean));
}
private static class PlayerTypeAdapter implements JsonSerializer<Player>
{
#Override
public JsonElement serialize(Player player, Type type,
JsonSerializationContext context)
{
throw new RuntimeException("I got called, woohoo");
}
}
public static interface Player //extends SupportsPropertyChanges
{
public String getName();
public void setName(String name);
}
// Simple implementation simulating what we're doing.
public static class MagicPlayer implements Player
{
private final String privateStuff = "secret";
private String name;
#Override
public String getName()
{
return name;
}
#Override
public void setName(String name)
{
this.name = name;
}
}
This gives:
{"privateStuff":"secret","name":"Alice"}
And of course, never calls my type adapter, which seemingly makes it impossible to get any other behaviour.
I had the same issue, with version 2.3.1 of Gson. I got it working by using
.registerTypeHierarchyAdapter(Player.class, new PlayerTypeAdapter())
instead of
.registerTypeAdapter(Player.class, new PlayerTypeAdapter())
Current release of GSON does not work this way. This will serialize your type using the adapter.
gson.toJson(bean, Player.class)
Alternatively you can register the PlayerTypeAdapter for MagicPlayer.class and your existing code will work. GSON looks up the TypeAdapter by the concrete (or top level) type only. So you will have to register a custom type adapter for every concrete type you may encounter.
There is TypeHeirarchyAdapter for when you want the same TypeAdapter to be used for all extending classes of a given type. I have not had good luck with this though and have not really spent time looking into why it has not worked well for me. Here is some relevant discussion: https://groups.google.com/forum/?fromgroups=#!searchin/google-gson/interface/google-gson/0ebOxifqwb0/OXSCNTiLpBQJ
Why it does not work:
PlayerTypeAdapter should implements JsonSerializer. Does it?
If it does - than should work. It work perfect for me, so don't see reason why it does not work for you. Start you application in debug, put breakpoints and checkout. There should be some reason.
In your test where is Gson builder with adapter? You should change it:
public void testJson() throws Exception
{
Player player = new MagicPlayer(); //BeanUtils.createDefault(Player.class);
player.setName("Alice");
// this is wrong if you want adapter
/* Gson gson = new Gson(); */
// this is what you nead
Gson gson = new GsonBuilder()
.registerTypeAdapter(Player.class, new PlayerTypeAdapter())
.create();
System.out.println(gson.toJson(bean));
}

How to Return Different Objects via Asynctask in Android

I figured this must be a common question, but I surprisingly couldn't find an answer, so maybe my entire structure is terribad...
I have an activity which downloads values/states of a game from a web service via AsyncTask. These values are used to update a custom view.
Once the view is created, various events from the view launch an AsyncTask to download other information.
This is functional, but the problem is I now have half a dozen AsyncTask classes in the activity with almost identical code. The only difference is the type of object that is returned (which is based on the json from the web service) and the method that is called from onPostExecute().
How can I use just two AsyncTask (one for post and one for get) without knowing what type of json object will be returned by the web service?
In a similar vein, how can I determine the type of object returned by the web service? The web service, if there is a problem, will return a json string that correlates to an ErrorMessage object rather than (for example) a GameData object.
Should I be using switch and instanceof in onPostExecute() somehow? Callbacks maybe?
You can use an abstract base class, which your related classes extends.
Sample code:
public abstract class IBaseObject {
protected String error;
public IBaseObject(String param) {
error = param;
}
public abstract String getError();
}
public class ObjectOne extends IBaseObject {
private String objectParam;
public ObjectOne(String error, String objectSpecificParam) {
super(error);
objectParam = objectSpecificParam;
}
#Override
public String getError() {
return error;
}
}
and for example, use it like this:
private class GetTask extends AsyncTask<String, Void, IBaseObject> {
protected IBaseObject doInBackground(String... url) {
// Get your data.
// Construct your corresponding object given by specific
// parameters from your JSON response.
if (a_parameter_match) {
return new ObjectOne(some_json_params...);
} else {
return new ObjectTwo(some_json_params...);
}
}
protected void onPostExecute(IBaseObject object) {
object.getError(); // Or whatever you need here.
}
}
This is just from the top of my head. I couldn't relate to your specific problem, although the ideas here should be enough to get you started on your new structure.
This is too long for a comment, so I'm writing an answer. However it was the advice of #Pompe de velo that got me on this track, so I am accepting that answer. I also left out some information from my question that could have been useful.
Anyway, as of right now I do not see any major downsides to this approach, but time ( or maybe another SO user ;] ) will tell...
Essentially I have assigned a constant to every type of object that the activity will try to get. The part that I left out was that the server only returns an error object on a 4xx-5xx http status code. In other words, I am certain to either get the object I am expecting or an error object and I can determine which I got from the status code. Then a switch sends the actual json string to the appropriate method that can manipulate the response as necessary.
Simplified pseudocode...
private void getGameData(){
new MyAsyncTask(this, MyAsyncTask.OBJ_GAME_DATA).execute();
}
static class MyAsyncTask extends AsyncTask<String, Integer, String> {
private int outputObjectType;
protected static final int OBJ_GAME_DATA = 0;
protected static final int OBJ_OTHER_DATA = 1;
protected static final int OBJ_DIFFERENT_DATA = 2;
protected static final int OBJ_SERVER_ERROR = 3;
MyAsyncTask(MyActivity activity, int expectedObject){
outputObjectType = expectedObject;
}
doInBackground(){
if(httpStatusCode >= 400){
outputObjectType = MyAsyncTask.OBJ_SERVER_ERROR;
}
return jsonStringFromServer;
}
onPostExecute(String json){
switch(outputObjectType){
case MyAsyncTask.OBJ_SERVER_ERROR:
serverError(json);
break;
case MyAsyncTask.OBJ_GAME_DATA:
processGameData(json);
break;
// ....
}
}
}
private void serverError(String json){
ServerError se = new Gson().fromJson(json, ServerError.class);
Log.d(TAG, se.getErrorMessage());
}
private void processGameData(String json){
GameData gd = new Gson().fromJson(json, GameData.class);
// .......
}
I think this is more less what #Pompe de velo was saying, however I am just making my a_parameter_match based on the status code rather than something within the json.
If this is flawed, I'd love to learn why !

Categories

Resources