mk-5 / gdx-fireapp

libGDX Firebase API
Apache License 2.0
63 stars 21 forks source link

pojo classes can have only String fields in order to work with database reading . #12

Closed kasandrop closed 2 years ago

kasandrop commented 5 years ago

For example:

GdxFIRDatabase.instance().inReference("users/"+userId)
.readValue(List.class, new DataCallback<List<User>>(){

  @MapConversion(User.class)
  @Override
  public void onData(List<User> data)
  {

 User  user=data.get(0);
    // Do something with  user
  }

  @Override
  public void onError(Exception e)
  {
    // Handle failure
  }
});

In order the above code to work the User class must not have boolean or int fields.

For example the following pojo class will cause error:

public class User{
  public String username;
  public String email;
boolean flag;
  public User(){}
  public User(String username, String email,boolean flag){
    this.username = username;
    this.email = email;
this.flag=flag
  }
}

I am getting the following error: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Class java.lang.Object.getClass()' on a null object reference at mk.gdx.firebase.deserialization.DataCallbackMitmConverter.onData(DataCallbackMitmConverter.java:91) at mk.gdx.firebase.deserialization.DataCallbackMitmConverter$GenericDataCallback.onData(DataCallbackMitmConverter.java:130) at mk.gdx.firebase.android.database.resolvers.DataCallbackOnDataResolver.resolve(DataCallbackOnDataResolver.java:39) at mk.gdx.firebase.android.database.queries.ReadValueQuery$SingleValueListener.onDataChange(ReadValueQuery.java:74) at com.google.firebase.database.zzp.onDataChange(Unknown Source:7) at com.google.android.gms.internal.firebase_database.zzfc.zza(Unknown Source:13) at com.google.android.gms.internal.firebase_database.zzgx.zzdr(Unknown Source:2) at com.google.android.gms.internal.firebase_database.zzhd.run(Unknown Source:71) at android.os.Handler.handleCallback(Handler.java:873) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:193) at android.app.ActivityThread.main(ActivityThread.java:6669) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

Once I get rid of boolean or int fields everything works smoothly

mk-5 commented 5 years ago

API has such functionality when you getting single-value and tell API you want to get a List, API wraps single value into the list. So in above code this is the case. This null pointer is bad for sure, API should redirect this fail to "onError" method, I'll fix that. But for now take a look at this few possibilities:

- In above code, you have all fields public and only this boolean is package-private, maybe this cause deserialization error? and this single-value is null because of that? or maybe there is no such user?

- You try to get one user by id, but you getting a list. Try to get only one user so instead of:

GdxFIRDatabase.instance().inReference("users/"+userId)
.readValue(List.class, .... )

try:

GdxFIRDatabase.instance().inReference("users/"+userId)
.readValue(User.class
kasandrop commented 5 years ago

What I did was that all my fields of User class are String and then I did some kind of type conversion.

public class User{
  public String username;
  public String email;
 public  String profileSaved;
  public User(){}
  public User(String username, String email,String profileSaved){
    this.username = username;
    this.email = email;
this.profileSaved=profileSaved;

  }
}
GdxFIRDatabase.instance().inReference("users/"+userId)
.readValue(List.class, new DataCallback<List<User>>(){

  @MapConversion(User.class)
  @Override
  public void onData(List<User> data)
  {

 User  user=data.get(0);
    // Do something with  user
boolean profileSaved=Boolean.parseBoolean( user.getProfileSaved()  ));
  }

  @Override
  public void onError(Exception e)
  {
    // Handle failure
  }
});

but you gave me a better idea . I will try:

GdxFIRDatabase.instance().inReference("users/"+userId)
.readValue(User.class)

Thanks again for your great work.

Daniel-Donev commented 5 years ago

Hi I have the same problem

I get from Firebase list of my game levels and if have some fields in Level.class different from String type return null pointer

in order to use @MapConversion I need to all of my fields to be String

Daniel-Donev commented 5 years ago
public class Level {
    public int id;
    public String type;

    public Level(){}
    public Level(int id, String type){
        this.id = id;
        this.type = type;
    }
}

Map<String, Level> LevelArray = new HashMap<String, Level>();
        Level Level1 = new Level(1, "1");
        LevelArray.put("a1", Level1);

        Level Level2 = new Level(5, "1");
        LevelArray.put("a5", Level2);

GdxFIRDatabase.inst()
                .inReference("system/LevelDB")
                .setValue(LevelArray);

GdxFIRDatabase.inst()
                .inReference("system/LevelDB")
                    .readValue(List.class)
                    .then(new Consumer<List<Level>>() {
                        @Override
                        @MapConversion(Level.class)
                        public void accept(List<Level> Levels) {
                              int count = 0;
                            while (Levels.size() > count) {
                                Gdx.app.log("test ", "my " + Levels.get(count).id);
                                count++;
                            }
                        }
                    });
mk-5 commented 5 years ago

@Daniel-Donev thanks for reporting!

I've checked that, and it looks like the problems are within the primitive types, please try without those. So instead of, public int id; please try public Integer id;

In the middle time, I'll check why primitives cause problems with deserialization

Daniel-Donev commented 5 years ago

Thanks for the fast response!

I try to change int to Integer but the problem is still there. They are recorded to the Firebase correct, but after deserialization my Level objects is null

mk-5 commented 5 years ago

I need some more information, your example looks like missing something.

What is the LevelArray object? I don't see initialization for it. To make that works it should be an instance of List, (ArrayList for example). libGDX Array object wouldn't work.

Anyway, if you'd like to create objects list, I think the good aproach is to use .push(). Like this:

GdxFIRDatabase.inst()
                .inReference("system/LevelDB")
                .push()
                .setValue(level1);

GdxFIRDatabase.inst()
                .inReference("system/LevelDB")
                .push()
                .setValue(level2);

If you need to do that sequentially, here is an example:

GdxFIRDatabase.inst().promise()
                .then( 
                     GdxFIRDatabase.inst()
                           .inReference("system/LevelDB")
                           .push()
                           .setValue(level1)
                ).then(
                     GdxFIRDatabase.inst()
                           .inReference("system/LevelDB")
                           .push()
                           .setValue(level2)
                );
mk-5 commented 5 years ago

@Daniel-Donev any updates on this? :) could you give me some more info? what is database structure? and how you try to get that Levels list which causes null values inside?

Daniel-Donev commented 5 years ago

Hi sorry for delay response. LevelArray is HashMap: HashMap<Integer, Level> LevelArray = new HashMap<Integer, Level>();

save it in Firebase with (all fine in save)

GdxFIRDatabase.inst()
                .inReference("system/LevelDB")
                .setValue(LevelArray);

If I have some fields in Level.class different from String type when a try to get them have return null pointer (try to change them to Integer as you suggest)

GdxFIRDatabase.inst()
                .inReference("system/LevelDB")
                    .readValue(List.class)
                    .then(new Consumer<List<Level>>() {
                        @Override
                        @MapConversion(Level.class)
                        public void accept(List<Level> Levels) {
                              int count = 0;
                            while (Levels.size() > count) {
                                Gdx.app.log("test ", "my " + Levels.get(count).id);
                                count++;
                            }
                        }
                    });

Gdx.app.log("test","" + Levels); ... return []

Daniel-Donev commented 5 years ago

This is a full example of my code:

public class Level {
    public Integer id;
    public Integer type;
    public Integer depend_id;

    public Level(Integer id, Integer type, Integer depend_id){
        this.id = id;
        this.type = type;
        this.depend_id = depend_id;
    }
}
HashMap<Integer, Level> LevelArray = new HashMap<Integer, Level>();
        Level Level1 = new Level(1, 1, 0);
        LevelArray.put(1, Level1);

        Level Level2 = new Level(5, 1, 1);
        LevelArray.put(5, Level2);

        GdxFIRDatabase.inst()
                .inReference("system/LevelDB")
                .setValue(LevelArray);

            GdxFIRDatabase.inst()
                .inReference("system/LevelDB")
                    .readValue(List.class)
                    .then(new Consumer<List<Level>>() {
                        @Override
                        @MapConversion(Level.class)
                        public void accept(List<Level> Levels) {
                           Gdx.app.log("test","" + Levels);
                        }
                    });
mk-5 commented 5 years ago

Okay, there are few things:

  1. Yep, I have also checked that, and libGDX JSON serialization/deserialization doesn't work well with primitives by default - I'm working right now to fix that, it should be merged soon.

  2. The list is empty, probably because you don't wait for the result from the first call. You may chain promises like this:

    GdxFIRDatabase.promise()
    .then(GdxFIRDatabase.inst().inReference("system/LevelDB").setValue(LevelArray))
    .then(GdxFIRDatabase.inst().inReference("system/LevelDB").readValue(List.class))
    .then(new Consumer<List<Level>>() {
        @Override
        @MapConversion(Level.class)
         public void accept(List<Level> Levels) {
             Gdx.app.log("test","" + Levels);
             success();
         }
     });
  3. Firebase SDK needs map keys to be String. So your HashMap<Integer, Level> is incorrect - Firebase database will not allow that. Supported data formats are (reference: https://firebase.google.com/docs/database/android/read-and-write):

image

So when you try to set Map with Integer as keys - java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String will be thrown

  1. Your Level class doesn't contain default constructor, so libGDX reflection will not be able to create it by reflection. There is no stuff like Jackson @JsonCreator here