2

I'm using the Task API in my app to retrieve data from Firebase Database, which is usually from different nodes. I have a helper class for Firebase Database like so:

public class FirebaseDbHelper {

    public Task<DataSnapshot> getData() {
        TaskCompletionSource<DataSnapshot> source = new TaskCompletionSource<>();
        DatabaseReference dbRef = FirebaseDatabase.getInstance().getReference(FIRST_NODE).child(SUB_NODE);
        dbRef.addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                source.setResult(dataSnapshot);
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {
                source.setException(databaseError.toException());
            }
        });
        return source.getTask();
    }

}

As you can see, getData() returns a Task object, which I use on my interactor class (I'm using the MVP architecture for my app) like so:

public class TestDbInteractor {

    private FirebaseDbHelper mDbHelper;
    private Listener mListener;

    public TestDbInteractor(@NonNull Listener listener) {
        mDbHelper = new FirebaseDbHelper();
        mListener = listener;
    }

    void getData() {
        mDbHelper.getData().addOnCompleteListener(task -> {
            if (task.isSuccessful()) {
                mListener.onGetDataSuccess(new MyObject(task.getResult()));
            } else {
                mListener.onGetDataFailed(task.getException());
            }
        });
    }

    public interface Listener {

        void onGetDataSuccess(MyObject object);

        void onGetDataFailed(Exception exception);

    }

}

This works as expected. However, we noticed a behavior that when retrieving a lot of data, even if the activity that started the task is already finish()ed, the task still proceeds and attempts to complete. This I believe, is something that could be considered as a memory leak, since a process is still going even though it's supposed to be stopped/destroyed already.

What's worse is that when I try to get a different data (using a different Task in a different activity to a different node in Firebase), we noticed that it waits for the previous task to complete first before proceeding with this new one.

To give more context, we're developing a chat app similar to Telegram, where users could have multiple rooms and the behavior we saw is happening when a user enters a room. This is the flow:

  1. User enters room, I request data for the room details.
  2. Upon getting the room details, I display it, then request for the messages. I only retrieve the most recent 10. During this time, I just show a progress bar on the activity.

In order for the message details to be complete, I get data from different nodes on Firebase, this is where I use Tasks mainly.

  1. After getting the messages, I pass it on to the View, to display the messages, then I attach a listener for new messages. Everything works as expected.

The behavior I mentioned at the beginning is noticeable when the user does something like this:

  1. User enters a room with messages, room details are retrieved instantly, messages are still loading.
  2. User leaves the room (presses the back button), this gets the user back to the room list, and enters a different one.

At this point, the retrieval of the room details takes such a long time - which we thought was odd, since the data isn't really that big to begin with.

After a few more testing, we concluded that the long retrieval time was caused by the current task (get room details) is still waiting for the previous task (get messages) started in a different activity, to finish first before starting.

I attempted to implement my answer here, trying to use a CancellableTask, but I am at a loss on how to use it with my current implementation, where I use a TaskCompletionSource, where you could only set a result or an exception.

I was thinking this could work if I move the task completion source to the interactor class level instead of the helper -- I haven't tried it yet. I think it's possible, but would take a lot of time to refactor the classes I already have.

So I figure why not try Doug's answer, using activity-scoped listeners. So I tested it like below.

In my activity, I added a getActivity() method, which can be called in the presenter:

public class TestPresenter
        implements TestDbInteractor.Listener {

    private View mView;

    private TestDbInteractor mDbInteractor;

    @Override
    void bindView(View view) {
        mView = view;

        mDbInteractor = new TestDbInteractor(this);
    }

    @Override
    void requestMessages() {
        mDbInteractor.getData(mView.getActivity());
    }

    // Listener stuff below

}

and updated my getData() like so:

void getData(@NonNull Activity activity) {
    mDbHelper.getData().addOnCompleteListener(activity, task -> {
        if (task.isSuccessful()) {
            mListener.onGetDataSuccess(new MyObject(task.getResult()));
        } else {
            mListener.onGetDataFailed(task.getException());
        }
    });
}

Unfortunately, this doesn't seem to work though, exiting the activity still waits for the tasks to complete, before the new task initiated in a different activity starts.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
AL.
  • 36,815
  • 10
  • 142
  • 281
  • 1
    Hey AL. It's hard to exactly know what's going on. By my guess is that you're *either* trying to cancel a listener before it has returned its initial data, which is not possible, *or* trying to stop a child* callback from firing midway through the data, which is not possible. But as said: it's hard to say more than that without a simple recipe to reproduce. For example: I would expect to be possible with either the task *or* firebase, it seems unlikely that this is an interaction between the two, since they're not really aware of each other. – Frank van Puffelen Apr 28 '18 at 04:03
  • Hi Puf. I actually saw your answer on cancelling listeners for Firebase DB and thought it is probably a behavior with the Tasks API itself. I guess I still need to do a simple app that can repro this behavior first for it to be more clear, but that would take much time. I'll try to set it up and would post a link here for the sample app if I manage to finish it. Thanks! – AL. Apr 28 '18 at 04:09

2 Answers2

2

If you kick off a query to Realtime Database, it will always run to completion, whether or not there are any listeners attached to the Task that was returned. There is no way to cancel that work, neither by removing the last listener manually, nor by using activity-scoped listeners that are removed automatically. Queries in motion stay in motion. Also, all traffic to and from RTDB is pipelined over a single socket, which implies that the results of subsequent queries after one that's incomplete will have to wait for the everything ahead of it in the queue to complete first. This is likely the root cause for your observation - you have an incomplete query that other queries are waiting on, regardless of your use of the Task API.

Fortunately, if you have persistence enabled, the second query should be served by the cache of the first query, and not require another round trip to the server.

If you need to make sure that you retain the results of the first query across configuration changes that destroy the activity, then you should use something like LiveData from the Android architecture components to manage this, so that you can pick up the query where it left off after a configuration change. If you do this, don't use activity-scoped listeners.

I've written a three-part blog post about using architecture components with Firebase, which may also be of interest.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • "*..all traffic to and from RTDB is pipelined over a single socket..*" -- Yup. This definitely explains the behavior we noticed. What's weird though is we actually do have persistence enabled in the app, so the part where the room details still loading for a time is weird -- but we'll try to have a better look at it. Thank you Doug! – AL. Apr 28 '18 at 04:53
0

Hey You can use childEventListener. use dataSnapshot.getChildrenCount().

    dbFriend=FirebaseDatabase.getInstance().getReference("Friend");
    dbFriend=dbFriend.child(mPreferences.getString("username","")).child("already");

    dbFriend.addChildEventListener(new ChildEventListener() {
        int already=0;
        @Override
        public void onChildAdded(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {

            Username u=dataSnapshot.getValue(Username.class);


            already=alread+1;
            if(already >= dataSnapshot.getChildrenCount()){

                //get to know when data fetching got completed

            }

        }

        @Override
        public void onChildChanged(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {

        }

        @Override
        public void onChildRemoved(@NonNull DataSnapshot dataSnapshot) {

        }

        @Override
        public void onChildMoved(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {

        }

        @Override
        public void onCancelled(@NonNull DatabaseError databaseError) {

        }
    });