我关注Android Firebase Codelab。
这是我试图修改的友好聊天应用的项目:https://github.com/firebase/friendlychat-android/tree/master/android
我希望在从数据库中检索邮件后填充MessageViewHolder 。不幸的是,现在,MessageViewHolder也会在本地填充(在用户按下' SEND'之后)。我不希望后者发生。这意味着视图持有者会更新两次(一次在本地,一次从数据库中检索),这是低效的。
侧节点:此外,当用户按下“发送”按钮时,用户可能会认为他在线。并看到他的MessageViewHolder填充了他的消息。那是不受欢迎的!
请转到MainActivity第177行查看populateViewHolder
方法。
我过去几个月一直试图回避这个问题,但现在我真的需要解决问题才能继续工作。 为实现我想要的目标,我必须做出哪些改变?
答案 0 :(得分:1)
我已经解释过这个场景了。我提供了两种解决方案。第二种解决方案是最简单的解决方案。
当您离线时更新本地mMessageRecyclerView
的原因是Firebase具有离线功能。 Firebase推/拉通过单独的工作线程发生。一旦你再次上线,这个工作线程就会开始同步 - 你可能需要考虑存储工作线程内容的持久性。重新启动应用后,所有本地写入都会消失。请注意,您已在主线程中设置了mFirebaseAdapter
,如下所示:
mFirebaseAdapter = new FirebaseRecyclerAdapter<FriendlyMessage, MessageViewHolder>(FriendlyMessage.class, R.layout.item_message,
MessageViewHolder.class,
mFirebaseDatabaseReference.child(MESSAGES_CHILD)) {
/* more stuffs here */ }
这意味着FirebaseRecyclerAdapter
的任何以下4个参数都会发生变化:
FriendlyMessage.class,
R.layout.item_message,
MessageViewHolder.class,
mFirebaseDatabaseReference.child(MESSAGES_CHILD)
mFirebaseAdapter
内的观察者立即感知到更改并立即更新您的mMessageRecyclerView
RecyclerView。因此,在成功更新Firebase之前,您必须保证不会更改任何这些参数。
另请注意,mFirebaseDatabaseReference.child(MESSAGES_CHILD).push().setValue(friendlyMessage);
,这里实际的推送 - 网络操作 - 发生在一个单独的工作线程中,如您所知,网络操作不会发生在主线程中,但下面的行mMessageEditText.setText("");
发生在主线程中。因此,偶数工作线程正在执行(成功或不成功),主线程已经更新了您的GUI。
所以,可能的解决方案是:
1)复杂解决方案 Github:https://github.com/uddhavgautam/UddhavFriendlyChat
(您必须保证在成功更新Firebase更新之前不要更改mFirebaseAdapter
的任何上述4个参数 - 所以这有点复杂,但仍能正常运行):在这里创建{{1 }},与FriendlyMessage2
完全相似,(仅FriendlyMessage
而不是FriendlyMessage
),并且只使用FriendlyMessage2
内的FriendlyMessage
,如下所示:
在此解决方案中,我们使用来自OkHttp客户端的REST写入,而不是使用onCompletionListener
更新Firebase数据库。这是因为setValue()
触发器会在本地更新您的mFirebaseDatabaseReference1.child(MESSAGES_CHILD).push()
,这就是为什么使用RecyclerView
无法在此处工作的原因。 setValue()
稍后发生,因此您可以在onCompletionListener()
中实现逻辑。使用REST写入,您的RecyclerViewAdapter会根据firebase数据进行更新。此外,我们需要在使用Okhttp客户端通过单独的工作线程编写之前序列化FriendlyMessage2。因此,您必须将build.gradle修改为
更新您的build.gradle
onSuccess()
您的setOnClickListener方法实现
compile 'com.squareup.okhttp3:okhttp:3.5.0'
compile 'com.google.code.gson:gson:2.8.0'
FriendlyMessage2.java - 您必须创建此项,因为您的RecyclerView适配器依赖于FriendlyMessage。使用FriendlyMessage,RecyclerView适配器中的观察者会感知到并因此更新您的视图。
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);
// }
// });
// }
}
});
我希望,你了解一切。
此处的简单解决方案:
2)简单的解决方案:您只需在点击按钮后立即使用OnLineTracker,如下所示。我更喜欢第二种方法。
它解决了: 1)MessageViewHolder不会在离线状态下填充。 2)只有在Firebase数据库写入发生时才会填充MessageViewHolder。
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;
}
}
<强> OnLineTracker.java 强>
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);
}
}
});
答案 1 :(得分:0)
使用事务发送消息,而不是setValue。这样你确定你在网上打电话,如果(没有检查,但应该是)监听器是ValueEventListener,那么这个监听器在离线时不会被调用。
如果添加了数据,你会看到这个,否则没有(如果没有在本地添加)创建事务时你有onComplete,你可以检查是否发送了消息
只需使用databaseReference.runTransaction(transactionHandler, false)
要确保只有在连接数据库时才运行事务,您可以在databaseReference.runTransaction
override fun doTransaction(p0: MutableData?): Transaction.Result {
p0?.value = objectToPut
return Transaction.success(p0)
}
有关交易的更多信息:https://firebase.google.com/docs/database/android/read-and-write#save_data_as_transactions
如何检查是否在线:
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");
}
});
答案 2 :(得分:0)
更新方法如下:
protected void populateViewHolder(int populatorId, final MessageViewHolder viewHolder,
FriendlyMessage friendlyMessage, int position);
现在这个方法的用户应该传递populatorId,在实现中你可以像:
一样处理它 @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
}
<强>更新强>
因此,此方法的用户可以检查网络状态,然后为其生成Id。那么你可以在实现中处理该Id。在调用此方法和示例之前检查互联网如果没有互联网使用populatorId = NO_INTERNET,那么你可以在实现中检查它并做任何事情。
答案 3 :(得分:0)
我对Firebase不再了解。我认为,如果您之前检查输入密钥是否可用互联网,那么您可以解决问题。如果互联网可用,那么执行firebase代码,否则给出一些消息或任何你想要的东西。其他想法是在更新FirebaseRecyclerAdapter之前查看请检查互联网是否可用。最后是每次调用firebase实例的每个地方都要检查。
答案 4 :(得分:0)
这是一个可选的解决方案。为什么不利用聊天消息适配器触发两次的事实。 (只要你不使用交易) 第一次本地调用时,使用id将消息保存在本地数据库中。适配器在聊天中显示消息,其进度轮运行如“发送消息...当第二次调用时,适配器从数据库中拉出聊天消息并将其设置为”已发送“,因此使用已发送的消息更新适配器。
通过这种方式,您可以在离线时进行聊天,就像Google视频群聊一样。这样,如果app重启/崩溃,所有未发送的消息都可以像“再次发送”选项一样。 此外,适配器现在可以正常显示突出显示的新消息和旧(已查看)消息。