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:
- User enters room, I request data for the room details.
- 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.
- 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:
- User enters a room with messages, room details are retrieved instantly, messages are still loading.
- 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.