问题
I'm following the Android Firebase Codelab.
Here's the project of the Friendly Chat app I'm attempting to modify: https://github.com/firebase/friendlychat-android/tree/master/android
I want the MessageViewHolder to be populated only after retrieving a message from the database. Unfortunately, right now, MessageViewHolder also gets populated locally (after the user presses 'SEND'). I don't want the latter to happen. That means the view holder gets updated twice (once locally and once after retrieval from the database) which is inefficient.
Side-node: Also, the user might think he's online when he presses 'SEND' and sees his MessageViewHolder get populated with his message. That's unwanted!
Please go to line 177 of MainActivity to see populateViewHolder
method.
I've been trying to dodge the issue for the last couple of months, but now I really need the problem to be solved to continue working. What changes do I have to make to achieve what I want?
回答1:
I have explained the scenario little bit. I provided two solutions in your case. The second solution is the easiest one.
The reason it updates your local mMessageRecyclerView
when you're offline is that Firebase has off-line capabilities. Firebase push/pull happens via separate worker thread. This worker thread starts synchronizing once you go online again -- you might need to think about persistence stororing the contents of worker threads. Once you restart your app, all local write goes off. Please note you have setup your mFirebaseAdapter
in the main thread as below:
mFirebaseAdapter = new FirebaseRecyclerAdapter<FriendlyMessage, MessageViewHolder>(FriendlyMessage.class, R.layout.item_message,
MessageViewHolder.class,
mFirebaseDatabaseReference.child(MESSAGES_CHILD)) {
/* more stuffs here */ }
It means any below 4 parameters of FirebaseRecyclerAdapter
changes:
FriendlyMessage.class,
R.layout.item_message,
MessageViewHolder.class,
mFirebaseDatabaseReference.child(MESSAGES_CHILD)
the observer inside mFirebaseAdapter
immediately senses that change and updates your mMessageRecyclerView
RecyclerView immediately. So, you have to guarantee you don't change any of these parameters until you update Firebase successfully.
Also note that, mFirebaseDatabaseReference.child(MESSAGES_CHILD).push().setValue(friendlyMessage);
, here the actual push -- network operation -- happens in a separate worker thread, as you know, network operation can't happen in Main Thread, but the below line mMessageEditText.setText("");
happens in Main Thread. So, the even worker thread is executing (successful or unsuccessful), the Main Thread already updated your GUI.
So, possible solutions are:
1) Complex solution Github: https://github.com/uddhavgautam/UddhavFriendlyChat
(you must guarantee that you don't change any above 4 parameters of your mFirebaseAdapter
until you successfully update your Firebase update -- so this is kind of complex but still works perfectly): Here you create FriendlyMessage2
, which is exactly similar to FriendlyMessage
, (only instead of FriendlyMessage
there is FriendlyMessage2
), and use that only use FriendlyMessage
inside onCompletionListener
as below:
In this solution, instead of updating Firebase database using setValue()
, we use REST write from OkHttp client. This is because mFirebaseDatabaseReference1.child(MESSAGES_CHILD).push()
triggers, which locally updates your RecyclerView
, that's why using setValue()
doesn't work here. The onCompletionListener()
happens later, so you can implement the logic inside onSuccess()
. Using REST write, your RecyclerViewAdapter updates based on firebase data. Also, we need to serialize the FriendlyMessage2 before we do write using Okhttp client via separate worker thread. So, you have to modify your build.gradle as
update your build.gradle
compile 'com.squareup.okhttp3:okhttp:3.5.0'
compile 'com.google.code.gson:gson:2.8.0'
Your setOnClickListener method implementation
mSendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
/*
new FirebaseRecyclerAdapter<FriendlyMessage, MessageViewHolder>(
FriendlyMessage.class,
R.layout.item_message,
MessageViewHolder.class,
mFirebaseDatabaseReference.child(MESSAGES_CHILD))
*/
// if (OnLineTracker.isOnline(getApplicationContext())) {
friendlyMessage2 = new FriendlyMessage2(mMessageEditText.getText().toString(), mUsername, mPhotoUrl, null);
JSON = MediaType.parse("application/json; charset=utf-8");
Gson gson = new GsonBuilder().setPrettyPrinting().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
myJson = gson.toJson(friendlyMessage2);
client = new OkHttpClient();
new Thread(new Runnable() {
@Override
public void run() {
try {
if(post(mFirebaseDatabaseReference.child(MESSAGES_CHILD).toString(), myJson, client, JSON)) {
/* again for GUI update, we call main thread */
runOnUiThread(new Runnable() {
@Override
public void run() {
mMessageEditText.setText("");
mFirebaseAnalytics.logEvent(MESSAGE_SENT_EVENT, null);
}
});
}
} catch (JSONException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
boolean post(String url, String json, OkHttpClient client, MediaType JSON) throws JSONException, IOException {
RequestBody body = RequestBody.create(JSON, new JSONObject(json).toString());
Request request = new Request.Builder()
.url(url+".json")
.post(body)
.build();
Response response = client.newCall(request).execute();
if(response.isSuccessful()) {
return true;
}
else return false;
}
}).start();
// mFirebaseDatabaseReference1.child(MESSAGES_CHILD).push().setValue(friendlyMessage2).addOnCompleteListener(new OnCompleteListener<Void>() {
// @Override
// public void onComplete(@NonNull Task<Void> task) {
// FriendlyMessage friendlyMessage = new FriendlyMessage(mMessageEditText.getText().toString(), mUsername, mPhotoUrl, null);
//
// mMessageEditText.setText("");
// mFirebaseAnalytics.logEvent(MESSAGE_SENT_EVENT, null);
// }
// });
// }
}
});
FriendlyMessage2.java -- you have to create this because your RecyclerView adapter is dependent on FriendlyMessage. Using FriendlyMessage, the observers inside RecyclerView adapter sense that and hence update your view.
package com.google.firebase.codelab.friendlychat;
/**
* Created by uddhav on 8/17/17.
*/
public class FriendlyMessage2 {
private String id;
private String text;
private String name;
private String photoUrl;
private String imageUrl;
public FriendlyMessage2() {
}
public FriendlyMessage2(String text, String name, String photoUrl, String imageUrl) {
this.text = text;
this.name = name;
this.photoUrl = photoUrl;
this.imageUrl = imageUrl;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhotoUrl() {
return photoUrl;
}
public void setPhotoUrl(String photoUrl) {
this.photoUrl = photoUrl;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
}
I hope, you understood everything.
Simple solution from here:
2) Easy solution: You simply use OnLineTracker just after you click on Button as below. I prefer 2nd method.
It solves: 1) MessageViewHolder doesn't get populated on offline. 2) MessageViewHolder gets populated only when Firebase Database write happens.
mSendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (OnLineTracker.isOnline(getApplicationContext())) {
FriendlyMessage friendlyMessage = new FriendlyMessage(mMessageEditText.getText().toString(), mUsername, mPhotoUrl, null);
mFirebaseDatabaseReference.child(MESSAGES_CHILD).push().setValue(friendlyMessage);
mMessageEditText.setText("");
mFirebaseAnalytics.logEvent(MESSAGE_SENT_EVENT, null);
}
}
});
OnLineTracker.java
public class OnLineTracker {
public static boolean isOnline(Context ctx) {
ConnectivityManager cm = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo netInfo = cm.getActiveNetworkInfo();
return netInfo != null && netInfo.isConnectedOrConnecting();
}
}
回答2:
Use transaction to send messages, not setValue. This way u're sure that u're making call online and if (didn't check this, but it should be) listener is ValueEventListener then this listener will not get called if offline.
If data gets added you will see this, otherwise no (if not added locally) When creating transaction you have onComplete where you can check if message was sent aswell
Just use databaseReference.runTransaction(transactionHandler, false)
To make sure that transaction runs only when db is connected u can check wheter firebase is connected before databaseReference.runTransaction
override fun doTransaction(p0: MutableData?): Transaction.Result {
p0?.value = objectToPut
return Transaction.success(p0)
}
More about transactions: https://firebase.google.com/docs/database/android/read-and-write#save_data_as_transactions
How to check if is online:
DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
boolean connected = snapshot.getValue(Boolean.class);
if (connected) {
System.out.println("connected");
} else {
System.out.println("not connected");
}
}
@Override
public void onCancelled(DatabaseError error) {
System.err.println("Listener was cancelled");
}
});
回答3:
Update method like this:
protected void populateViewHolder(int populatorId, final MessageViewHolder viewHolder,
FriendlyMessage friendlyMessage, int position);
Now user of this method should pass populatorId and in the implementation you can handle it like:
@Override
protected void populateViewHolder(int populatorId, final MessageViewHolder viewHolder,
FriendlyMessage friendlyMessage, int position) {
if( populatorId == FROM_DATABASE ){
//the implementation
mProgressBar.setVisibility(ProgressBar.INVISIBLE);
if (friendlyMessage.getText() != null) {
viewHolder.messageTextView.setText(friendlyMessage.getText());
viewHolder.messageTextView.setVisibility(TextView.VISIBLE);
viewHolder.messageImageView.setVisibility(ImageView.GONE);
.
.
.
}else{
//do nothing
}
UPDATE
Therefore user of this method can check network state and then generate Id for it. then you can handle that Id in implementation. For check internet before calling this method and example if there is no internet use populatorId=NO_INTERNET then you can check it in implementation and do what ever.
回答4:
I have no more idea about Firebase. I think that if you check before to hit the enter key that internet is available or not then you can fix the issue. If internet is available then execute firebase code else give some message or any thing other that you want. Other think is before update FirebaseRecyclerAdapter view please check that internet is available or not. Finally is to check every to every places wherever you call firebase instance.
回答5:
Here´s an optional solution. Why not take advantage of the fact that the chat message adapter fires twice. (as long as you dont use a transaction) First time the local call you save the message in local db with an id. The adapter presents the message in the chat with a progress wheel running like "sending message... When the second call fire the adapter pull the chat message from the db and sets it as "sent" and hence update the adapter with the sent message.
This way you can chat while offline just like Google hangout. This way if app restart/crash all not sent messages can have like a "send again" option. Also adapter can now easily show new messages highlighted and old(viewed) messages as normal.
来源:https://stackoverflow.com/questions/44855654/only-populate-viewholder-with-messages-retrieved-from-database