Skip to content

Commit 4143a0a

Browse files
authored
Refactor Event Listeners (#289)
* Move ExecutorEventListener to core * Rename ExecutorEventListener to AsyncEventListener Make it match iOS. * Move ListenerRegistrationImpl to core * Refactor ListenerRegistration creation * Pull activity-scoping out ListenerRegistrationImpl--it's a totally separate concern. * Make the relationship between the various levels of listener clearer in the places where we construct them.
1 parent 29fa815 commit 4143a0a

File tree

5 files changed

+135
-95
lines changed

5 files changed

+135
-95
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
import com.google.android.gms.tasks.Tasks;
2727
import com.google.firebase.annotations.PublicApi;
2828
import com.google.firebase.firestore.FirebaseFirestoreException.Code;
29+
import com.google.firebase.firestore.core.ActivityScope;
30+
import com.google.firebase.firestore.core.AsyncEventListener;
2931
import com.google.firebase.firestore.core.EventManager.ListenOptions;
32+
import com.google.firebase.firestore.core.ListenerRegistrationImpl;
3033
import com.google.firebase.firestore.core.QueryListener;
3134
import com.google.firebase.firestore.core.UserData.ParsedSetData;
3235
import com.google.firebase.firestore.core.UserData.ParsedUpdateData;
@@ -37,9 +40,7 @@
3740
import com.google.firebase.firestore.model.mutation.DeleteMutation;
3841
import com.google.firebase.firestore.model.mutation.Precondition;
3942
import com.google.firebase.firestore.util.Assert;
40-
import com.google.firebase.firestore.util.ExecutorEventListener;
4143
import com.google.firebase.firestore.util.Executors;
42-
import com.google.firebase.firestore.util.ListenerRegistrationImpl;
4344
import com.google.firebase.firestore.util.Util;
4445
import java.util.Map;
4546
import java.util.concurrent.ExecutionException;
@@ -311,6 +312,7 @@ private Task<DocumentSnapshot> getViaSnapshotListener(Source source) {
311312
options.includeDocumentMetadataChanges = true;
312313
options.includeQueryMetadataChanges = true;
313314
options.waitForSyncWhenOnline = true;
315+
314316
ListenerRegistration listenerRegistration =
315317
addSnapshotListenerInternal(
316318
// No need to schedule, we just set the task result directly
@@ -479,49 +481,58 @@ public ListenerRegistration addSnapshotListener(
479481
*
480482
* <p>Will be Activity scoped if the activity parameter is non-null.
481483
*
482-
* @param executor The executor to use to call the listener.
484+
* @param userExecutor The executor to use to call the listener.
483485
* @param options The options to use for this listen.
484486
* @param activity Optional activity this listener is scoped to.
485-
* @param listener The event listener that will be called with the snapshots.
487+
* @param userListener The user-supplied event listener that will be called with document
488+
* snapshots.
486489
* @return A registration object that can be used to remove the listener.
487490
*/
488491
private ListenerRegistration addSnapshotListenerInternal(
489-
Executor executor,
492+
Executor userExecutor,
490493
ListenOptions options,
491494
@Nullable Activity activity,
492-
EventListener<DocumentSnapshot> listener) {
493-
ExecutorEventListener<ViewSnapshot> wrappedListener =
494-
new ExecutorEventListener<>(
495-
executor,
496-
(snapshot, error) -> {
497-
if (snapshot != null) {
498-
Assert.hardAssert(
499-
snapshot.getDocuments().size() <= 1,
500-
"Too many documents returned on a document query");
501-
Document document = snapshot.getDocuments().getDocument(key);
502-
DocumentSnapshot documentSnapshot;
503-
if (document != null) {
504-
boolean hasPendingWrites = snapshot.getMutatedKeys().contains(document.getKey());
505-
documentSnapshot =
506-
DocumentSnapshot.fromDocument(
507-
firestore, document, snapshot.isFromCache(), hasPendingWrites);
508-
} else {
509-
// We don't raise `hasPendingWrites` for deleted documents.
510-
boolean hasPendingWrites = false;
511-
documentSnapshot =
512-
DocumentSnapshot.fromNoDocument(
513-
firestore, key, snapshot.isFromCache(), hasPendingWrites);
514-
}
515-
listener.onEvent(documentSnapshot, null);
516-
} else {
517-
Assert.hardAssert(error != null, "Got event without value or error set");
518-
listener.onEvent(null, error);
519-
}
520-
});
495+
EventListener<DocumentSnapshot> userListener) {
496+
497+
// Convert from ViewSnapshots to DocumentSnapshots.
498+
EventListener<ViewSnapshot> viewListener =
499+
(snapshot, error) -> {
500+
if (error != null) {
501+
userListener.onEvent(null, error);
502+
return;
503+
}
504+
505+
Assert.hardAssert(snapshot != null, "Got event without value or error set");
506+
Assert.hardAssert(
507+
snapshot.getDocuments().size() <= 1,
508+
"Too many documents returned on a document query");
509+
510+
Document document = snapshot.getDocuments().getDocument(key);
511+
DocumentSnapshot documentSnapshot;
512+
if (document != null) {
513+
boolean hasPendingWrites = snapshot.getMutatedKeys().contains(document.getKey());
514+
documentSnapshot =
515+
DocumentSnapshot.fromDocument(
516+
firestore, document, snapshot.isFromCache(), hasPendingWrites);
517+
} else {
518+
// We don't raise `hasPendingWrites` for deleted documents.
519+
documentSnapshot =
520+
DocumentSnapshot.fromNoDocument(
521+
firestore, key, snapshot.isFromCache(), /* hasPendingWrites= */ false);
522+
}
523+
userListener.onEvent(documentSnapshot, null);
524+
};
525+
526+
// Call the viewListener on the userExecutor.
527+
AsyncEventListener<ViewSnapshot> asyncListener =
528+
new AsyncEventListener<>(userExecutor, viewListener);
529+
521530
com.google.firebase.firestore.core.Query query = asQuery();
522-
QueryListener queryListener = firestore.getClient().listen(query, options, wrappedListener);
523-
return new ListenerRegistrationImpl(
524-
firestore.getClient(), queryListener, activity, wrappedListener);
531+
QueryListener queryListener = firestore.getClient().listen(query, options, asyncListener);
532+
533+
return ActivityScope.bind(
534+
activity,
535+
new ListenerRegistrationImpl(firestore.getClient(), queryListener, asyncListener));
525536
}
526537

527538
@Override

firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@
2525
import com.google.android.gms.tasks.Tasks;
2626
import com.google.firebase.annotations.PublicApi;
2727
import com.google.firebase.firestore.FirebaseFirestoreException.Code;
28+
import com.google.firebase.firestore.core.ActivityScope;
29+
import com.google.firebase.firestore.core.AsyncEventListener;
2830
import com.google.firebase.firestore.core.Bound;
2931
import com.google.firebase.firestore.core.EventManager.ListenOptions;
3032
import com.google.firebase.firestore.core.Filter;
3133
import com.google.firebase.firestore.core.Filter.Operator;
34+
import com.google.firebase.firestore.core.ListenerRegistrationImpl;
3235
import com.google.firebase.firestore.core.OrderBy;
3336
import com.google.firebase.firestore.core.QueryListener;
3437
import com.google.firebase.firestore.core.RelationFilter;
@@ -39,9 +42,7 @@
3942
import com.google.firebase.firestore.model.value.FieldValue;
4043
import com.google.firebase.firestore.model.value.ReferenceValue;
4144
import com.google.firebase.firestore.model.value.ServerTimestampValue;
42-
import com.google.firebase.firestore.util.ExecutorEventListener;
4345
import com.google.firebase.firestore.util.Executors;
44-
import com.google.firebase.firestore.util.ListenerRegistrationImpl;
4546
import com.google.firebase.firestore.util.Util;
4647
import java.util.ArrayList;
4748
import java.util.List;
@@ -891,30 +892,37 @@ public ListenerRegistration addSnapshotListener(
891892
* @param executor The executor to use to call the listener.
892893
* @param options The options to use for this listen.
893894
* @param activity Optional activity this listener is scoped to.
894-
* @param listener The event listener that will be called with the snapshots.
895+
* @param userListener The user-supplied event listener that will be called with the snapshots.
895896
* @return A registration object that can be used to remove the listener.
896897
*/
897898
private ListenerRegistration addSnapshotListenerInternal(
898899
Executor executor,
899900
ListenOptions options,
900901
@Nullable Activity activity,
901-
EventListener<QuerySnapshot> listener) {
902-
ExecutorEventListener<ViewSnapshot> wrappedListener =
903-
new ExecutorEventListener<>(
904-
executor,
905-
(@Nullable ViewSnapshot snapshot, @Nullable FirebaseFirestoreException error) -> {
906-
if (snapshot != null) {
907-
QuerySnapshot querySnapshot = new QuerySnapshot(this, snapshot, firestore);
908-
listener.onEvent(querySnapshot, null);
909-
} else {
910-
hardAssert(error != null, "Got event without value or error set");
911-
listener.onEvent(null, error);
912-
}
913-
});
914-
915-
QueryListener queryListener = firestore.getClient().listen(query, options, wrappedListener);
916-
return new ListenerRegistrationImpl(
917-
firestore.getClient(), queryListener, activity, wrappedListener);
902+
EventListener<QuerySnapshot> userListener) {
903+
904+
// Convert from ViewSnapshots to QuerySnapshots.
905+
EventListener<ViewSnapshot> viewListener =
906+
(@Nullable ViewSnapshot snapshot, @Nullable FirebaseFirestoreException error) -> {
907+
if (error != null) {
908+
userListener.onEvent(null, error);
909+
return;
910+
}
911+
912+
hardAssert(snapshot != null, "Got event without value or error set");
913+
914+
QuerySnapshot querySnapshot = new QuerySnapshot(this, snapshot, firestore);
915+
userListener.onEvent(querySnapshot, null);
916+
};
917+
918+
// Call the viewListener on the userExecutor.
919+
AsyncEventListener<ViewSnapshot> asyncListener =
920+
new AsyncEventListener<>(executor, viewListener);
921+
922+
QueryListener queryListener = firestore.getClient().listen(query, options, asyncListener);
923+
return ActivityScope.bind(
924+
activity,
925+
new ListenerRegistrationImpl(firestore.getClient(), queryListener, asyncListener));
918926
}
919927

920928
@Override
Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,19 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package com.google.firebase.firestore.util;
15+
package com.google.firebase.firestore.core;
1616

1717
import static com.google.firebase.firestore.util.Assert.hardAssert;
1818

1919
import android.app.Activity;
2020
import android.support.v4.app.FragmentActivity;
2121
import com.google.firebase.firestore.ListenerRegistration;
22-
import com.google.firebase.firestore.core.FirestoreClient;
23-
import com.google.firebase.firestore.core.QueryListener;
24-
import com.google.firebase.firestore.core.ViewSnapshot;
2522
import java.util.ArrayList;
2623
import java.util.List;
2724
import javax.annotation.Nullable;
2825

2926
/**
30-
* Implements the ListenerRegistration interface by removing a query from the listener.
27+
* Scopes the lifetime of a ListenerRegistration to an Activity.
3128
*
3229
* <p>Regarding activity-scoped listeners, Android provides lifecycle callbacks (eg onStop()) that
3330
* custom `Activity`s can implement via subclassing. But we can't take advantage of that, since we
@@ -48,17 +45,9 @@
4845
* for lifecycle callbacks instead of creating/attaching a Fragment.
4946
* </ol>
5047
*/
51-
public class ListenerRegistrationImpl implements ListenerRegistration {
48+
public class ActivityScope {
5249

53-
private final FirestoreClient client;
54-
55-
/** The internal query listener object that is used to unlisten from the query. */
56-
private final QueryListener queryListener;
57-
58-
/** The event listener for the query that raises events asynchronously. */
59-
private final ExecutorEventListener<ViewSnapshot> asyncEventListener;
60-
61-
static class CallbackList {
50+
private static class CallbackList {
6251
void run() {
6352
for (Runnable callback : callbacks) {
6453
if (callback != null) {
@@ -142,7 +131,7 @@ private static <T> T castFragment(Class<T> fragmentClass, @Nullable Object fragm
142131
* everything in this function is deprecated.
143132
*/
144133
@SuppressWarnings("deprecation")
145-
private void onActivityStopCallOnce(Activity activity, Runnable callback) {
134+
private static void onActivityStopCallOnce(Activity activity, Runnable callback) {
146135
hardAssert(
147136
!(activity instanceof FragmentActivity),
148137
"onActivityStopCallOnce must be called with a *non*-FragmentActivity Activity.");
@@ -170,7 +159,7 @@ private void onActivityStopCallOnce(Activity activity, Runnable callback) {
170159
});
171160
}
172161

173-
private void onFragmentActivityStopCallOnce(FragmentActivity activity, Runnable callback) {
162+
private static void onFragmentActivityStopCallOnce(FragmentActivity activity, Runnable callback) {
174163
activity.runOnUiThread(
175164
() -> {
176165
StopListenerSupportFragment fragment =
@@ -194,28 +183,16 @@ private void onFragmentActivityStopCallOnce(FragmentActivity activity, Runnable
194183
});
195184
}
196185

197-
/** Creates a new ListenerRegistration. Is activity-scoped if and only if activity is non-null. */
198-
public ListenerRegistrationImpl(
199-
FirestoreClient client,
200-
QueryListener queryListener,
201-
@Nullable Activity activity,
202-
ExecutorEventListener<ViewSnapshot> asyncEventListener) {
203-
this.client = client;
204-
this.queryListener = queryListener;
205-
this.asyncEventListener = asyncEventListener;
206-
186+
/** Binds the given registration to the lifetime of the activity. */
187+
public static ListenerRegistration bind(
188+
@Nullable Activity activity, ListenerRegistration registration) {
207189
if (activity != null) {
208190
if (activity instanceof FragmentActivity) {
209-
onFragmentActivityStopCallOnce((FragmentActivity) activity, this::remove);
191+
onFragmentActivityStopCallOnce((FragmentActivity) activity, registration::remove);
210192
} else {
211-
onActivityStopCallOnce(activity, this::remove);
193+
onActivityStopCallOnce(activity, registration::remove);
212194
}
213195
}
214-
}
215-
216-
@Override
217-
public void remove() {
218-
asyncEventListener.mute();
219-
client.stopListening(queryListener);
196+
return registration;
220197
}
221198
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package com.google.firebase.firestore.util;
15+
package com.google.firebase.firestore.core;
1616

1717
import com.google.firebase.firestore.EventListener;
1818
import com.google.firebase.firestore.FirebaseFirestoreException;
@@ -23,13 +23,13 @@
2323
* A wrapper event listener that uses an Executor to dispatch events. Exposes a mute() call to
2424
* immediately silence the event listener when events are dispatched on different threads.
2525
*/
26-
public class ExecutorEventListener<T> implements EventListener<T> {
26+
public class AsyncEventListener<T> implements EventListener<T> {
2727
private final Executor executor;
2828
private final EventListener<T> eventListener;
2929

3030
private volatile boolean muted = false;
3131

32-
public ExecutorEventListener(Executor executor, EventListener<T> eventListener) {
32+
public AsyncEventListener(Executor executor, EventListener<T> eventListener) {
3333
this.executor = executor;
3434
this.eventListener = eventListener;
3535
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2018 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.core;
16+
17+
import com.google.firebase.firestore.ListenerRegistration;
18+
19+
/** Implements the ListenerRegistration interface by removing a query from the listener. */
20+
public class ListenerRegistrationImpl implements ListenerRegistration {
21+
22+
private final FirestoreClient client;
23+
24+
/** The internal query listener object that is used to unlisten from the query. */
25+
private final QueryListener queryListener;
26+
27+
/** The event listener for the query that raises events asynchronously. */
28+
private final AsyncEventListener<ViewSnapshot> asyncEventListener;
29+
30+
public ListenerRegistrationImpl(
31+
FirestoreClient client,
32+
QueryListener queryListener,
33+
AsyncEventListener<ViewSnapshot> asyncEventListener) {
34+
this.client = client;
35+
this.queryListener = queryListener;
36+
this.asyncEventListener = asyncEventListener;
37+
}
38+
39+
@Override
40+
public void remove() {
41+
asyncEventListener.mute();
42+
client.stopListening(queryListener);
43+
}
44+
}

0 commit comments

Comments
 (0)