How does Firebase sync work, with shared data? - java

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.
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 ?

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

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.

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 !

Related

I don't see any data in my Realtime Database in Firebase's console

I'm trying to figure out how to use Firebase properly, so I made this simple code, saving a "game" object with code and password. I also made it show the game's key on the screen. running the code, it works and shows what looks like a key on the screen, but I can't find any of the data anywhere on my Firebase Console. Here's the code:
public class testthing extends AppCompatActivity implements View.OnClickListener {
EditText pass;
TextView showkey;
EditText code;
Button create;
String codestr;
String passstr;
FirebaseDatabase firebaseDatabase;
DatabaseReference gameRef;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_testthing);
pass = findViewById(R.id.pass);
code = findViewById(R.id.code);
create = findViewById(R.id.create);
showkey = findViewById(R.id.showthingy);
firebaseDatabase = FirebaseDatabase.getInstance();
create.setOnClickListener(this);
}
#Override
public void onClick(View view) {
if(view==create){
codestr=code.getText().toString();
passstr=pass.getText().toString();
Game g = new Game(codestr,passstr,"");
gameRef = firebaseDatabase.getReference("gameRooms").push();
g.key = gameRef.getKey();
gameRef.setValue(g);
showkey.setText(g.key);
}
}
}
with the "game" class being:
#IgnoreExtraProperties
public class Game {
public String key;
public String code;
public String password;
public Game(){
}
public Game(String code, String password,String key){
this.code = code;
this.password = password;
this.key = key;
}
}
When you're using the following line of code:
gameRef.setValue(g);
The data you're trying to write into the database may succeed or fail, but you'll never know that since you aren't attaching a listener to check that. So solve this, you have to attach a listener as in the following lines of code:
gameRef.setValue(g).addOnCompleteListener(new OnCompleteListener<Void>() {
#Override
public void onComplete(#NonNull Task<Void> task) {
if (task.isSuccessful()) {
Log.d("TAG", "The operation is complete.");
showkey.setText(g.key);
} else {
Log.d("TAG", task.getException().getMessage());
}
}
});
If the operation fails, most likely the Firebase servers rejected your operation. So make sure you have the proper rules. Otherwise, set the key to the TextView. Remember that all Firebase APIs are asynchronous. This includes the write and the read operations. If you need later to read the data, I recommend you check the following article:
How to read data from Firebase Realtime Database using get()?

How to update only specific field on Firebase Database on android

i was trying to update only one field when user clicks a button
here is the database
i need to update report_status when user clicks a button
b1.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
report_value=Integer.parseInt(arrayforreport.get(posi));
report_value++;
databaseReference.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
//i need to update report_status by id
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
});
In order to update a value in your Firebase database there is no need to attach a listener, that is needed only when you need to read data. So to update a particular node, you only need to use setValue() method directly on the databaseReference object.
So assuming that the type node is a direct child of your Firebase database, please use the following line of code:
DatabaseReference rootRef = FirebaseDatabase.getInstance().getReference();
rootRef.child("type")
.child("Whatsapp")
.child("Shopping Deals")
.child(shoppingDealId)
.child("report_status")
.setValue("newValue");
In which shoppingDealId is the id of a particular shopping deal. You can get this id using the following line of code:
String key = ref.push().getKey();
addValueEventListener is to retrieve the value, you don't need it to set the value.
First get the key from DatabaseReference then use setValue to change the value.
b1.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
report_value=Integer.parseInt(arrayforreport.get(posi));
report_value++;
DatabaseReference databaseReference = adapter.getRef(position);
String key = databaseReference.getKey();
databaseReference.child("type").child("Whatsapp").child("Shopping Deals").child(key).child("report_status").setValue(report_status);
}
});

How can I check if my Firebase database has data for a certain user? (Android/Java)

When a user makes an account with firebase on my android app, they then need to be directed to a screen where they can add some separate information like username, for example. The app knows to switch the activity through an AuthStateListener, so when an account is made or someone signs in the code here is executed (no intents are changed here right now, keep reading to see why):
mAuthListener = new FirebaseAuth.AuthStateListener() {
#Override
public void onAuthStateChanged(#NonNull FirebaseAuth firebaseAuth) {
FirebaseUser user = firebaseAuth.getCurrentUser();
if (user != null) {
// User is signed in
Log.d(TAG, "onAuthStateChanged:signed_in:" + user.getUid());
userID = user.getUid();
toastMessage("User has signed in: " + user.getEmail());
} else {
// User is signed out
Log.d(TAG, "onAuthStateChanged:signed_out");
}
// ...
}
};
In the if block where it is checking to see if user does not equal to null, this is where I need to change the intent to another activity where they will enter their username. Basically the logic is, if the user already has a username and they are just signing in then they will be directed to the home screen activity, and if they are signing up they need to be directed to the activity where they make a username. To know which activity I need to send them to I need to have the code look in the firebase database and see if their account has a username already or not. I cannot figure out how to read data from the firebase in this situation, it seems as if the only way to read from the database (according to others) is through an onDataChange methods like so:
mRef.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
Log.d(TAG, "This: "+dataSnapshot.getValue());
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
which uses dataSnapshots. the problem with this is this code is only fired when data is added to the database, therefor this method will not execute any code when a user is simply signing up and has never added a username to the database. How can I query to see data from the database just to check if a certain user has any data there?
OnDatachange is fired in all of cases, if user add data, remove data, change data ect.
To check is user already have username do this inside OnDatachange:
if(dataSnapshot.exists()){
//GO TO OTHER ACTIVITY
]else{
//GO TO SET USER DATA ACTIVITY
]
See this exemple of one of my implementations:
if (user !=null){
DatabaseReference myRef = database.getReference("users").child(user.getUid()).child("user_bio");
myRef.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()){
bio.setText(dataSnapshot.getValue(String.class));
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
In this exemple if user_bio exists then set text to userbio if not do nothing. In the first time that user log in in my app this method do nothing but when user add bio this method set the bio text in userbio text. but if i want to show some text if user hasnt bio i simply add na else and set text to the bio for exemple:
if (user !=null){
DatabaseReference myRef = database.getReference("users").child(user.getUid()).child("user_bio");
myRef.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()){
bio.setText(dataSnapshot.getValue(String.class));
}else{
bio.setText("no user bio");
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
Have you try to use addValueEventListener inside onCreate method?
If you run an activity while the listener inside onCreate method it will fired once when you open an activity. So i think you shouldn't have a problem here.

Fetching data from firebase taking more than one button click to display

I want to display data which is fetched from firebase whenever user clicks a button. This is the code which I am using currently
todayPoem = (TextView) findViewById(R.id.todpoem);
/*Firebase related code*/
myFirebaseRef = new Firebase("https://firebase url");
myFirebaseRef.child("poem").addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot snapshot) {
System.out.println(snapshot.getValue());
s1 = (String) snapshot.getValue();
}
#Override
public void onCancelled(FirebaseError error) {
}
});
}
public void onClickPoem(View v) {
todayPoem.setTextSize(20);
todayPoem.setText(s1);
}
And the xml file contains a textview and a button to fetch data from the firebase. But the problem is whenever I click the button the data is not displayed instantly, usually it takes almost 5-6 button clicks for the data to be displayed. What am I doing wrong here?
I had this problem before... I would advice you doing it like this example:
mrf.setAndroidContext(this);
mrf = new Firebase('Firebase url');
final Firebase myData = mrf.child("info");
myData.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(final DataSnapshot dataSnapshot) {
x1 = dataSnapshot.getValue(String.class);
x2 = x1; // Global variable
}
#Override
public void onCancelled(FirebaseError firebaseError) {
}
});
use variable 'x2' to do what you want. I don't why but there is some problem with using the same variable that was used to receive Data from Firebase.
Hope it work with you...
Also if you would like the user to know that data are being retrieve you can use progressBar with Handler. After a button is clicked and 'x1' equals null do something like this: progressBar.setVisibility(View.VISIBLE);
then
handler.postDelayed(new Runnable() {
public void run() {
progressBar.setVisibility(View.GONE);
t.setText(x2);
}
}, 800);
Good luck..

How to use results from a Firebase query to ultimately populate a ListView in Android Studio

I found that my ListView is not updated when the app is run because the adapter is set before the Firebase query occurs.
First Question: Is there anyway to set a listener for query completion? This way I can set the adapter there instead. I know some Firebase methods auto-generate onComplete()/onSuccess() listeners.
For debugging purposes I can trigger an update to the ListView by re-entering onResume(). This is where I receive an error message:
The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. Make sure your adapter calls notifyDataSetChanged() when its content changes. [in ListView(16908298, class android.widget.ListView) with Adapter(class android.widget.ArrayAdapter)]
To my understanding this error means I cannot add items to my ArrayList "usernames" inside the onChildAdded() method if I use usernames in my ArrayAdapter.
Second Question: How else can I store the values returned from my Firebase query and use those to ultimately populate a ListView without directly changing its contents from a "background thread"?
My attempt:
public class EditFriendsActivity extends AppCompatActivity {
protected ArrayList<String> usernames = new ArrayList<>();
#Override
protected void onResume() {
super.onResume();
Firebase usernameRef = ref.child("users");
Query queryRef = usernameRef.orderByKey();
queryRef.addChildEventListener(new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
String username = (String) dataSnapshot.child("username").getValue();
usernames.add(username);
Log.v(TAG, usernames + "");
}
#Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
Log.v(TAG, "Inside ChildChanged");
}
#Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
Log.v(TAG, "Inside ChildRemoved");
}
#Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
Log.v(TAG, "Inside ChildMoved");
}
#Override
public void onCancelled(FirebaseError firebaseError) {
Log.e(TAG, firebaseError.getMessage());
AlertDialog.Builder builder = new AlertDialog.Builder(EditFriendsActivity.this);
builder.setTitle(R.string.error_title)
.setMessage(firebaseError.getMessage())
.setPositiveButton(android.R.string.ok, null);
AlertDialog dialog = builder.create();
dialog.show();
}
});
// Array adapter
ArrayAdapter<String> adapter = new ArrayAdapter<>(
EditFriendsActivity.this,
android.R.layout.simple_list_item_multiple_choice,
usernames);
mFriendsList.setAdapter(adapter);
}
}
Updated Code: Fixed typo in listener from marked answer and cleared list each time.
Firebase userRef = ref.child("users");
userRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
usernames.clear(); // Clear list before updating
for (DataSnapshot child : dataSnapshot.getChildren()) {
String username = (String) child.child("username").getValue();
usernames.add(username);
Log.v(TAG, usernames + "");
}
Follow Up Question Here
Is there anyway to set a listener for query completion? This way I can set the adapter there instead. I know some Firebase methods auto-generate onComplete()/onSuccess() listeners.
Firebases synchronizes data from a backend to your application. So unlike a traditional database that has a request/response flow, Firebase queries don't really complete. That's why in the FirebaseUI library we keep an open listener and notify the ListView of any changes.
That said: if you just want to load the data from a location once, you can use addSingleValueEventListener:
queryRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot snapshot) {
for (DataSnapshot child: snapshot.getChildren()) {
String username = (String) child.child("username").getValue();
usernames.add(username);
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(
EditFriendsActivity.this,
android.R.layout.simple_list_item_multiple_choice,
usernames);
mFriendsList.setAdapter(adapter);
}
#Override
public void onCancelled(FirebaseError firebaseError) {
Log.e(TAG, firebaseError.getMessage());
}
});
There are probably typos in the above, but this is the general gist of it.
You can simply use a Query
Query ref = FirebaseDatabase.getInstance().getReference()
.child("users").orderByChild("name");

Categories

Resources