Skip to content

Commit f2cef8c

Browse files
authored
feat(firestore): add support for listening snapshot from cache (#12585)
* feat(firestore): add support for listening snapshot from cache * web * android * android * ios * add tests * fix ios * android * windows * format * add tests and document reference tests
1 parent 8966f48 commit f2cef8c

38 files changed

+464
-105
lines changed

packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,7 @@ public void querySnapshot(
894894
@NonNull GeneratedAndroidFirebaseFirestore.PigeonQueryParameters parameters,
895895
@NonNull GeneratedAndroidFirebaseFirestore.PigeonGetOptions options,
896896
@NonNull Boolean includeMetadataChanges,
897+
@NonNull GeneratedAndroidFirebaseFirestore.ListenSource source,
897898
@NonNull GeneratedAndroidFirebaseFirestore.Result<String> result) {
898899
Query query =
899900
PigeonParser.parseQuery(getFirestoreFromPigeon(app), path, isCollectionGroup, parameters);
@@ -914,14 +915,16 @@ public void querySnapshot(
914915
query,
915916
includeMetadataChanges,
916917
PigeonParser.parsePigeonServerTimestampBehavior(
917-
options.getServerTimestampBehavior()))));
918+
options.getServerTimestampBehavior()),
919+
PigeonParser.parseListenSource(source))));
918920
}
919921

920922
@Override
921923
public void documentReferenceSnapshot(
922924
@NonNull GeneratedAndroidFirebaseFirestore.FirestorePigeonFirebaseApp app,
923925
@NonNull GeneratedAndroidFirebaseFirestore.DocumentReferenceRequest parameters,
924926
@NonNull Boolean includeMetadataChanges,
927+
@NonNull GeneratedAndroidFirebaseFirestore.ListenSource source,
925928
@NonNull GeneratedAndroidFirebaseFirestore.Result<String> result) {
926929
FirebaseFirestore firestore = getFirestoreFromPigeon(app);
927930
DocumentReference documentReference =
@@ -935,6 +938,7 @@ public void documentReferenceSnapshot(
935938
documentReference,
936939
includeMetadataChanges,
937940
PigeonParser.parsePigeonServerTimestampBehavior(
938-
parameters.getServerTimestampBehavior()))));
941+
parameters.getServerTimestampBehavior()),
942+
PigeonParser.parseListenSource(source))));
939943
}
940944
}

packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/GeneratedAndroidFirebaseFirestore.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,35 @@ private Source(final int index) {
104104
}
105105
}
106106

107+
/**
108+
* The listener retrieves data and listens to updates from the local Firestore cache only. If the
109+
* cache is empty, an empty snapshot will be returned. Snapshot events will be triggered on cache
110+
* updates, like local mutations or load bundles.
111+
*
112+
* <p>Note that the data might be stale if the cache hasn't synchronized with recent server-side
113+
* changes.
114+
*/
115+
public enum ListenSource {
116+
/**
117+
* The default behavior. The listener attempts to return initial snapshot from cache and
118+
* retrieve up-to-date snapshots from the Firestore server. Snapshot events will be triggered on
119+
* local mutations and server side updates.
120+
*/
121+
DEFAULT_SOURCE(0),
122+
/**
123+
* The listener retrieves data and listens to updates from the local Firestore cache only. If
124+
* the cache is empty, an empty snapshot will be returned. Snapshot events will be triggered on
125+
* cache updates, like local mutations or load bundles.
126+
*/
127+
CACHE(1);
128+
129+
final int index;
130+
131+
private ListenSource(final int index) {
132+
this.index = index;
133+
}
134+
}
135+
107136
public enum ServerTimestampBehavior {
108137
/** Return null for [FieldValue.serverTimestamp()] values that have not yet */
109138
NONE(0),
@@ -1776,12 +1805,14 @@ void querySnapshot(
17761805
@NonNull PigeonQueryParameters parameters,
17771806
@NonNull PigeonGetOptions options,
17781807
@NonNull Boolean includeMetadataChanges,
1808+
@NonNull ListenSource source,
17791809
@NonNull Result<String> result);
17801810

17811811
void documentReferenceSnapshot(
17821812
@NonNull FirestorePigeonFirebaseApp app,
17831813
@NonNull DocumentReferenceRequest parameters,
17841814
@NonNull Boolean includeMetadataChanges,
1815+
@NonNull ListenSource source,
17851816
@NonNull Result<String> result);
17861817

17871818
/** The codec used by FirebaseFirestoreHostApi. */
@@ -2476,6 +2507,7 @@ public void error(Throwable error) {
24762507
PigeonQueryParameters parametersArg = (PigeonQueryParameters) args.get(3);
24772508
PigeonGetOptions optionsArg = (PigeonGetOptions) args.get(4);
24782509
Boolean includeMetadataChangesArg = (Boolean) args.get(5);
2510+
ListenSource sourceArg = ListenSource.values()[(int) args.get(6)];
24792511
Result<String> resultCallback =
24802512
new Result<String>() {
24812513
public void success(String result) {
@@ -2496,6 +2528,7 @@ public void error(Throwable error) {
24962528
parametersArg,
24972529
optionsArg,
24982530
includeMetadataChangesArg,
2531+
sourceArg,
24992532
resultCallback);
25002533
});
25012534
} else {
@@ -2516,6 +2549,7 @@ public void error(Throwable error) {
25162549
FirestorePigeonFirebaseApp appArg = (FirestorePigeonFirebaseApp) args.get(0);
25172550
DocumentReferenceRequest parametersArg = (DocumentReferenceRequest) args.get(1);
25182551
Boolean includeMetadataChangesArg = (Boolean) args.get(2);
2552+
ListenSource sourceArg = ListenSource.values()[(int) args.get(3)];
25192553
Result<String> resultCallback =
25202554
new Result<String>() {
25212555
public void success(String result) {
@@ -2530,7 +2564,7 @@ public void error(Throwable error) {
25302564
};
25312565

25322566
api.documentReferenceSnapshot(
2533-
appArg, parametersArg, includeMetadataChangesArg, resultCallback);
2567+
appArg, parametersArg, includeMetadataChangesArg, sourceArg, resultCallback);
25342568
});
25352569
} else {
25362570
channel.setMessageHandler(null);

packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/streamhandler/DocumentSnapshotsStreamHandler.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
import com.google.firebase.firestore.DocumentReference;
1212
import com.google.firebase.firestore.DocumentSnapshot;
1313
import com.google.firebase.firestore.FirebaseFirestore;
14+
import com.google.firebase.firestore.ListenSource;
1415
import com.google.firebase.firestore.ListenerRegistration;
1516
import com.google.firebase.firestore.MetadataChanges;
17+
import com.google.firebase.firestore.SnapshotListenOptions;
1618
import io.flutter.plugin.common.EventChannel.EventSink;
1719
import io.flutter.plugin.common.EventChannel.StreamHandler;
1820
import io.flutter.plugins.firebase.firestore.utils.ExceptionConverter;
@@ -27,24 +29,31 @@ public class DocumentSnapshotsStreamHandler implements StreamHandler {
2729
MetadataChanges metadataChanges;
2830

2931
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior;
32+
ListenSource source;
3033

3134
public DocumentSnapshotsStreamHandler(
3235
FirebaseFirestore firestore,
3336
DocumentReference documentReference,
3437
Boolean includeMetadataChanges,
35-
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior) {
38+
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior,
39+
ListenSource source) {
3640
this.firestore = firestore;
3741
this.documentReference = documentReference;
3842
this.metadataChanges =
3943
includeMetadataChanges ? MetadataChanges.INCLUDE : MetadataChanges.EXCLUDE;
4044
this.serverTimestampBehavior = serverTimestampBehavior;
45+
this.source = source;
4146
}
4247

4348
@Override
4449
public void onListen(Object arguments, EventSink events) {
50+
SnapshotListenOptions.Builder optionsBuilder = new SnapshotListenOptions.Builder();
51+
optionsBuilder.setMetadataChanges(metadataChanges);
52+
optionsBuilder.setSource(source);
53+
4554
listenerRegistration =
4655
documentReference.addSnapshotListener(
47-
metadataChanges,
56+
optionsBuilder.build(),
4857
(documentSnapshot, exception) -> {
4958
if (exception != null) {
5059
Map<String, String> exceptionDetails = ExceptionConverter.createDetails(exception);

packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/streamhandler/QuerySnapshotsStreamHandler.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010

1111
import com.google.firebase.firestore.DocumentChange;
1212
import com.google.firebase.firestore.DocumentSnapshot;
13+
import com.google.firebase.firestore.ListenSource;
1314
import com.google.firebase.firestore.ListenerRegistration;
1415
import com.google.firebase.firestore.MetadataChanges;
1516
import com.google.firebase.firestore.Query;
17+
import com.google.firebase.firestore.SnapshotListenOptions;
1618
import io.flutter.plugin.common.EventChannel.EventSink;
1719
import io.flutter.plugin.common.EventChannel.StreamHandler;
1820
import io.flutter.plugins.firebase.firestore.utils.ExceptionConverter;
@@ -28,21 +30,29 @@ public class QuerySnapshotsStreamHandler implements StreamHandler {
2830
MetadataChanges metadataChanges;
2931
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior;
3032

33+
ListenSource source;
34+
3135
public QuerySnapshotsStreamHandler(
3236
Query query,
3337
Boolean includeMetadataChanges,
34-
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior) {
38+
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior,
39+
ListenSource source) {
3540
this.query = query;
3641
this.metadataChanges =
3742
includeMetadataChanges ? MetadataChanges.INCLUDE : MetadataChanges.EXCLUDE;
3843
this.serverTimestampBehavior = serverTimestampBehavior;
44+
this.source = source;
3945
}
4046

4147
@Override
4248
public void onListen(Object arguments, EventSink events) {
49+
SnapshotListenOptions.Builder optionsBuilder = new SnapshotListenOptions.Builder();
50+
optionsBuilder.setMetadataChanges(metadataChanges);
51+
optionsBuilder.setSource(source);
52+
4353
listenerRegistration =
4454
query.addSnapshotListener(
45-
metadataChanges,
55+
optionsBuilder.build(),
4656
(querySnapshot, exception) -> {
4757
if (exception != null) {
4858
Map<String, String> exceptionDetails = ExceptionConverter.createDetails(exception);

packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/PigeonParser.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import com.google.firebase.firestore.FieldPath;
1515
import com.google.firebase.firestore.Filter;
1616
import com.google.firebase.firestore.FirebaseFirestore;
17+
import com.google.firebase.firestore.ListenSource;
1718
import com.google.firebase.firestore.Query;
1819
import com.google.firebase.firestore.Source;
1920
import io.flutter.plugins.firebase.firestore.GeneratedAndroidFirebaseFirestore;
@@ -116,6 +117,18 @@ public static GeneratedAndroidFirebaseFirestore.DocumentChangeType toPigeonDocum
116117
}
117118
}
118119

120+
public static ListenSource parseListenSource(
121+
GeneratedAndroidFirebaseFirestore.ListenSource source) {
122+
switch (source) {
123+
case DEFAULT_SOURCE:
124+
return ListenSource.DEFAULT;
125+
case CACHE:
126+
return ListenSource.CACHE;
127+
default:
128+
throw new IllegalArgumentException("Unknown ListenSource value: " + source);
129+
}
130+
}
131+
119132
public static GeneratedAndroidFirebaseFirestore.PigeonDocumentSnapshot toPigeonDocumentSnapshot(
120133
com.google.firebase.firestore.DocumentSnapshot documentSnapshot,
121134
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior) {

packages/cloud_firestore/cloud_firestore/example/integration_test/document_reference_e2e.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,29 @@ void runDocumentReferenceTests() {
8989
});
9090
});
9191

92+
testWidgets('listens to a single response from cache', (_) async {
93+
DocumentReference<Map<String, dynamic>> document =
94+
await initializeTest('document-snapshot');
95+
Stream<DocumentSnapshot<Map<String, dynamic>>> stream =
96+
document.snapshots(source: ListenSource.cache);
97+
StreamSubscription<DocumentSnapshot<Map<String, dynamic>>>?
98+
subscription;
99+
100+
subscription = stream.listen(
101+
expectAsync1(
102+
(DocumentSnapshot<Map<String, dynamic>> snapshot) {
103+
expect(snapshot.exists, isFalse);
104+
},
105+
count: 1,
106+
reason: 'Stream should only have been called once.',
107+
),
108+
);
109+
110+
addTearDown(() async {
111+
await subscription?.cancel();
112+
});
113+
});
114+
92115
testWidgets('listens to multiple documents', (_) async {
93116
DocumentReference<Map<String, dynamic>> doc1 =
94117
await initializeTest('document-snapshot-1');

packages/cloud_firestore/cloud_firestore/example/integration_test/query_e2e.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,32 @@ void runQueryTests() {
272272
});
273273
});
274274

275+
testWidgets('listens to a single response from cache', (_) async {
276+
CollectionReference<Map<String, dynamic>> collection =
277+
await initializeTest('get-single-cache');
278+
await collection.add({'foo': 'bar'});
279+
Stream<QuerySnapshot<Map<String, dynamic>>> stream =
280+
collection.snapshots(source: ListenSource.cache);
281+
StreamSubscription<QuerySnapshot<Map<String, dynamic>>>? subscription;
282+
283+
subscription = stream.listen(
284+
expectAsync1(
285+
(QuerySnapshot<Map<String, dynamic>> snapshot) {
286+
expect(snapshot.docs.length, equals(1));
287+
expect(snapshot.docs[0], isA<QueryDocumentSnapshot>());
288+
QueryDocumentSnapshot<Map<String, dynamic>> documentSnapshot =
289+
snapshot.docs[0];
290+
expect(documentSnapshot.data()['foo'], equals('bar'));
291+
},
292+
count: 1,
293+
reason: 'Stream should only have been called once.',
294+
),
295+
);
296+
addTearDown(() async {
297+
await subscription?.cancel();
298+
});
299+
});
300+
275301
testWidgets('listens to multiple queries', (_) async {
276302
CollectionReference<Map<String, dynamic>> collection1 =
277303
await initializeTest('document-snapshot-1');

packages/cloud_firestore/cloud_firestore/example/ios/Flutter/AppFrameworkInfo.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
<key>CFBundleVersion</key>
2222
<string>1.0</string>
2323
<key>MinimumOSVersion</key>
24-
<string>11.0</string>
24+
<string>12.0</string>
2525
</dict>
2626
</plist>

packages/cloud_firestore/cloud_firestore/example/ios/Podfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Uncomment this line to define a global platform for your project
2-
platform :ios, '11.0'
2+
platform :ios, '12.0'
33

44
require 'yaml'
55

packages/cloud_firestore/cloud_firestore/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@
142142
9705A1C41CF9048500538489 /* Embed Frameworks */,
143143
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
144144
279EA8199A12C4F77765546D /* [CP] Embed Pods Frameworks */,
145-
7C56E6EB2829AAB5C53203A3 /* [CP] Copy Pods Resources */,
146145
);
147146
buildRules = (
148147
);
@@ -159,7 +158,7 @@
159158
97C146E61CF9000F007C117D /* Project object */ = {
160159
isa = PBXProject;
161160
attributes = {
162-
LastUpgradeCheck = 1430;
161+
LastUpgradeCheck = 1510;
163162
ORGANIZATIONNAME = "";
164163
TargetAttributes = {
165164
97C146ED1CF9000F007C117D = {
@@ -235,23 +234,6 @@
235234
shellPath = /bin/sh;
236235
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
237236
};
238-
7C56E6EB2829AAB5C53203A3 /* [CP] Copy Pods Resources */ = {
239-
isa = PBXShellScriptBuildPhase;
240-
buildActionMask = 2147483647;
241-
files = (
242-
);
243-
inputFileListPaths = (
244-
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
245-
);
246-
name = "[CP] Copy Pods Resources";
247-
outputFileListPaths = (
248-
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
249-
);
250-
runOnlyForDeploymentPostprocessing = 0;
251-
shellPath = /bin/sh;
252-
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
253-
showEnvVarsInLog = 0;
254-
};
255237
9740EEB61CF901F6004384FC /* Run Script */ = {
256238
isa = PBXShellScriptBuildPhase;
257239
alwaysOutOfDate = 1;
@@ -364,7 +346,7 @@
364346
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
365347
GCC_WARN_UNUSED_FUNCTION = YES;
366348
GCC_WARN_UNUSED_VARIABLE = YES;
367-
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
349+
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
368350
MTL_ENABLE_DEBUG_INFO = NO;
369351
SDKROOT = iphoneos;
370352
SUPPORTED_PLATFORMS = iphoneos;
@@ -442,7 +424,7 @@
442424
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
443425
GCC_WARN_UNUSED_FUNCTION = YES;
444426
GCC_WARN_UNUSED_VARIABLE = YES;
445-
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
427+
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
446428
MTL_ENABLE_DEBUG_INFO = YES;
447429
ONLY_ACTIVE_ARCH = YES;
448430
SDKROOT = iphoneos;
@@ -491,7 +473,7 @@
491473
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
492474
GCC_WARN_UNUSED_FUNCTION = YES;
493475
GCC_WARN_UNUSED_VARIABLE = YES;
494-
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
476+
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
495477
MTL_ENABLE_DEBUG_INFO = NO;
496478
SDKROOT = iphoneos;
497479
SUPPORTED_PLATFORMS = iphoneos;

0 commit comments

Comments
 (0)