7

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?

Harsh
  • 243
  • 3
  • 20
  • Thanks for asking me to help but unfortunatelly i cannot figure it out. – Alex Mamo Jul 01 '17 at 08:21
  • @AlexMamo Okay. What about this question: https://stackoverflow.com/questions/44860074/servervalue-timestamp-returning-2-values –  Jul 01 '17 at 10:49
  • The correct link to the project of the Friendly Chat app is: https://github.com/firebase/friendlychat-android/tree/master/android –  Aug 10 '17 at 13:19
  • For [MainActivity](https://github.com/firebase/friendlychat-android/blob/master/android/app/src/main/java/com/google/firebase/codelab/friendlychat/MainActivity.java) –  Aug 10 '17 at 13:19
  • 1
    I see nothing wrong in the code. that means it populates only on database items updated or added or removed. but not locally. – ugur Aug 10 '17 at 20:14
  • @uguboz http://i.imgur.com/8nn2V1L.gif –  Aug 13 '17 at 17:51
  • @uguboz The following recording is of a device in airplane mode. That means it has no access to the Internet and the database doesn't receive anything when the user presses 'SEND'. But, as you can see the viewHolder gets populated (after pressing 'SEND') even though it's not connected! i.imgur.com/8nn2V1L.gif There's a reason the question has +300 bounty –  Aug 13 '17 at 17:51
  • then show us how did you modify the code – ugur Aug 13 '17 at 18:07
  • @uguboz I did not modify the code! Try it yourself... Build the APK. –  Aug 13 '17 at 18:17
  • 1
    I can't try now but in the image u share it seems it updates once not twice. I don't remember but if it updates offline, firebase offline data persistence may be enabled. – ugur Aug 13 '17 at 19:26

5 Answers5

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();
    }
}
Uddhav P. Gautam
  • 7,362
  • 3
  • 47
  • 64
0

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");
  }
});
Janusz Hain
  • 597
  • 5
  • 18
  • Is there something more efficient? I'm sure there's a *very* simple fix to this. –  Aug 15 '17 at 14:48
  • I quote: "The following recording is of a device in airplane mode. That means it has no access to the Internet and the database doesn't receive anything when the user presses 'SEND'. But, as you can see the viewHolder gets populated (after pressing 'SEND') even though it's not connected! https://i.imgur.com/8nn2V1L.gif There's a reason the question has +300 bounty" –  Aug 15 '17 at 15:39
  • @Pro yes and it gets populated, because adding value works offline. Solution above will not add value while offline, so pressing "SEND" will not populate viewholder. I don't know if there is more efficient way, was looking for one, but didn;t find it on stackoverflow so I experimented with transaction and here is the only solution that works so far and isn't "that bad" – Janusz Hain Aug 15 '17 at 17:08
  • I understand. Can you please add an example (that corresponds with the project) where you use transaction to send messages, instead of setValue ? –  Aug 15 '17 at 17:27
  • https://github.com/firebase/friendlychat-android/blob/master/android/app/src/main/java/com/google/firebase/codelab/friendlychat/MainActivity.java 320 mFirebaseDatabaseReference.child(MESSAGES_CHILD).push().setValue(friendlyMessage); You need to push, this gives you the key. Then instead of setValue you will have to implement transaction as in my answer. You have to check before if firebase is online and do transaction only if you are online, because transaction would be called offline too. If online it will add value and notify value listener only once (not twice) – Janusz Hain Aug 15 '17 at 17:31
0

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.

Mahdi
  • 6,139
  • 9
  • 57
  • 109
  • As an experienced programmer, I can see that a straight-forward solution exists. Is there something more efficient? I'm sure there's a *very* simple fix to this. –  Aug 15 '17 at 14:48
  • I quote: "The following recording is of a device in airplane mode. That means it has no access to the Internet and the database doesn't receive anything when the user presses 'SEND'. But, as you can see the viewHolder gets populated (after pressing 'SEND') even though it's not connected! https://i.imgur.com/8nn2V1L.gif There's a reason the question has +300 bounty" –  Aug 15 '17 at 15:39
  • How can one update `populateViewHolder` method exactly? Where can we find it? What are the steps to do that? Please say it clearly in your answer. –  Aug 15 '17 at 17:25
0

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.

Ninja
  • 678
  • 10
  • 26
0

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.

Erik
  • 5,039
  • 10
  • 63
  • 119