How does Firebase sync work, with shared data?
Asked Answered
D

3

4

I use Firebase to handle the Auth topic of my Android app. I also save a user profile on Firebase, that contain user id and extra options that user can update in the android app.

At startup, the app check the auth, and auth. It reload the user profile (on Firebase) to then update my userInstance on the app.

Firebase is set as offline capable at app level. The userInstance POJO is sync with Firebase at log and stop to be sync at unlog.

I have a special parameter, in my user profile that I can change (as an admin).

Every-time I update it on the Firebase console, it is replaced by the previous value when the app start again.

How can this happen ?

BTW : 1/ Based on which mechanism are the data merged, if multiple client have different local values ?

Here is simpler code, where I tried to reproduce the error. :

MyApplication.java

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Firebase.setAndroidContext(this);
        Firebase.getDefaultConfig().setLogLevel(Logger.Level.DEBUG);
        Firebase.getDefaultConfig().setPersistenceEnabled(true);


    }
}

MainActivity

public class MainActivity extends AppCompatActivity {

    Firebase ref;
    User user;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ref = new Firebase("https://millezim-test.firebaseIO.com").child("user");
        ref.keepSynced(true);

        Button br = (Button) findViewById(R.id.b_read);
        Button bs = (Button) findViewById(R.id.b_save);
        final TextView tv_r = (TextView) findViewById(R.id.tv_value_toread);
        final EditText tv_s = (EditText) findViewById(R.id.tv_value_tosave);

        user = new User();

        bs.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!tv_s.getText().toString().equalsIgnoreCase(""))
                    user.setAge(Integer.valueOf(tv_s.getText().toString()));
                ref.setValue(user);
            }
        });

        br.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ref.addListenerForSingleValueEvent(new ValueEventListener() {
                    @Override
                    public void onDataChange(DataSnapshot dataSnapshot) {
                        User u = dataSnapshot.getValue(User.class);
                        if (u != null)
                            tv_r.setText(String.valueOf(u.getAge()));
                        else
                            tv_r.setText("Bad Value");
                    }

                    @Override
                    public void onCancelled(FirebaseError firebaseError) {

                    }
                });
            }
        });

        ref.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                User u = dataSnapshot.getValue(User.class);
                u.setCounter(u.getCounter() + 1);
                user = u;
                saveUser();
            }

            @Override
            public void onCancelled(FirebaseError firebaseError) {

            }
        });
    }

    public void saveUser() {
        ref.setValue(user);
    }
}

If you just change a value in the app, then the counter inc until the app stop : this is normal. But what is strand is the age pass from old to new then to old cyclically without stopping.

enter image description here

And I feel that behavior in my app, without the cyclic, as I do not have a counter, but I cannot change a parameter in the admin client, I always get back the previous value stored in the mobile.

I just Auth, then I update my UserInstance with AuthData + the User fetch from Firebase (probably the cached data), and then I save back the updated User under Firebase (As I may got new AuthData, and I normally get the latest User from Firebase)

2/ In this simple example, I saw that if I read the value at start, it fetch the data cached in the app. How can I force having online data ?

Discommodity answered 21/10, 2015 at 13:19 Comment(2)
Instead of describing your code, include the minimal code to reproduce your problem in the question. See stackoverflow.com/help/mcveCheloid
IMHO, system question, as concept question are not always better explained with code ? By the way I created a simple app, that triggered an issue, that seems related to my problem.Discommodity
C
10

The problem comes from the fact that you're using disk persistence with a single-value event listener. When you do:

ref.addListenerForSingleValueEvent(new ValueEventListener() {...

You're asking for a single value of that location (the user in your case). If Firebase has a value for that location in its local cache, it will fire for that value straight away.

The best way to solve this is to not use a single-value listener, but instead use a regular event listener. That way you will get two events: one for the cached version and one for the version that comes back from the server (if it is different).

The only alternative is to not use Firebase's disk persistence. Without that, there won't be a local cache for the data to be read from upon a restart.

There were a few discussions about this combination on the Firebase mailing list. Here's one: https://groups.google.com/forum/#!msg/firebase-talk/ptTtEyBDKls/XbNKD_K8CQAJ

Cheloid answered 21/10, 2015 at 21:59 Comment(9)
ok thanks for the link, it help to understand. So I understand your advise to solve the issues, I will make a try. However I'm not sure to understand why this is toggling between the old and new value infinitely ! see my coming understanding scenario.Discommodity
This happen like this ? 1. GetValue : old (singleValueListener) 11. SaveValue : old 2. GetValue : new (because it is keepsync(true)) 21. SaveValue : new If the 2 is register before the 11 is accomplished, then it loops !Discommodity
changing Single to Normal listener didn't solve the problemDiscommodity
setting Firebase.getDefaultConfig().setPersistenceEnabled(false); didn't solve the problem !Discommodity
setting ref.keepSync(false) didn't solve the problemDiscommodity
removing everything (one of the 2 listener) reach the same behavior, as soon as you just update back to the server a value you receive. The fact that a loop is created, is not good, but is planed in the coded behavior (because of the counter++), however the fact that the age is toggling make no sens at all for me !Discommodity
any comment or hints from you ?Discommodity
Does this mean that I can always expect regular event listener to fire twice irrespective of the cache sync state? And the second time it will always be latest value from server?Sagittal
@Sagittal no I think. Second trigger will happen only if synced data is different from previously triggered cached data.Nares
D
1

After digesting a bit, my current strategy is to use Firebase for my data persistance, and not use anymore my own objects. (Before I had to sync UI, my data, the firebase cache, and the server data)

So now, I use

  • use disk caching
  • use onValueEventListener
    • keep update data (only to be read with component that need sync data)
    • trigger event that update UI (for component that can accept async data)
  • define some specific setter, that update data on Firebase (not anymore at app level)

It does means, that when I update a data, it goes to the server (or Firebase caching layer) until it goes back to the UI. As firebase handle this caching, if fast this is fast enough, and this is Firebase that deal with network sync.

Discommodity answered 2/11, 2015 at 20:13 Comment(0)
D
0

To bring the (1) solution from @frank-van-puffelen into code :

public class MainActivity extends AppCompatActivity {

    Firebase ref;
    User user;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ref = new Firebase("https://test.firebaseIO.com").child("user");
        ref.keepSynced(true);

        Button br = (Button) findViewById(R.id.b_read);
        Button bs = (Button) findViewById(R.id.b_save);
        final TextView tv_r = (TextView) findViewById(R.id.tv_value_toread);
        final EditText tv_s = (EditText) findViewById(R.id.tv_value_tosave);

        user = new User();

        bs.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!tv_s.getText().toString().equalsIgnoreCase(""))
                    user.setAge(Integer.valueOf(tv_s.getText().toString()));
                ref.setValue(user);
            }
        });

        br.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ref.addValueEventListener(new ValueEventListener() {
                    @Override
                    public void onDataChange(DataSnapshot dataSnapshot) {
                        User u = dataSnapshot.getValue(User.class);
                        if (u != null)
                            tv_r.setText(String.valueOf(u.getAge()));
                        else
                            tv_r.setText("Bad Value");
                    }

                    @Override
                    public void onCancelled(FirebaseError firebaseError) {

                    }
                });
            }
        });

        ref.addValueEventListener(new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                User u = dataSnapshot.getValue(User.class);
                u.setCounter(u.getCounter() + 1);
                user = u;
                saveUser();
            }

            @Override
            public void onCancelled(FirebaseError firebaseError) {

            }
        });
    }

    public void saveUser() {
        ref.setValue(user);
    }
}

However, this change nothing, even worst. Now it seems that every value set, stay as a ghost value (hold by client/server request), and the value toggling can be seen with every values set !

EDIT

The following code worked out ! Having a normal ValueListener, that you stopped before saving again a value, and you enable back when save is completed ! (Ok I was thinking this may be done in the Firebase Framework).

public class MainActivity extends AppCompatActivity {

    Firebase ref;
    User user;
    private ValueEventListener theListener;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ref = new Firebase("https://test.firebaseIO.com").child("user");
        ref.keepSynced(false);

        Button bs = (Button) findViewById(R.id.b_save);
        final EditText tv_s = (EditText) findViewById(R.id.tv_value_tosave);

        user = new User();

        bs.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!tv_s.getText().toString().equalsIgnoreCase(""))
                    user.setAge(Integer.valueOf(tv_s.getText().toString()));
                ref.setValue(user);
            }
        });


        theListener = new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                User u = dataSnapshot.getValue(User.class);
                u.setCounter(u.getCounter() + 1);
                user = u;
                updateUI(u);
                saveUser();
            }

            @Override
            public void onCancelled(FirebaseError firebaseError) {

            }
        };

        ref.addValueEventListener(theListener);
    }

    public void saveUser() {
        ref.removeEventListener(theListener);
        ref.setValue(user, new Firebase.CompletionListener() {
            @Override
            public void onComplete(FirebaseError firebaseError, Firebase firebase) {
                ref.addValueEventListener(theListener);
            }
        });
    }

    public void updateUI(User user) {

        TextView tv_r = (TextView) findViewById(R.id.tv_value_toread);
        if (user != null)
            tv_r.setText(String.valueOf(user.getAge()));
        else
            tv_r.setText("Bad Value");
    }
}

EDIT

However this do not allow to change a value on the admin page. The age value is set and then remain back to the one that is save on the mobile.

So I imaging then the only solution is to solve at system level. DO NOT USE VALUELISTENER FOR VALUE THAT AN APP CAN SAVE AND THAT CAN BE SAVED BY THIRD PARTY APP. Please advise/correct this assumption !

Discommodity answered 22/10, 2015 at 9:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.