diff --git a/packages/cloud_firestore/CHANGELOG.md b/packages/cloud_firestore/CHANGELOG.md index f6f2e143f416..f5d647e0b9db 100644 --- a/packages/cloud_firestore/CHANGELOG.md +++ b/packages/cloud_firestore/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.12.9 + +* New optional `includeMetadataChanges` parameter added to `DocumentReference.snapshots()` + and `Query.snapshots()` +* Fix example app crash when the `message` field was not a string +* Internal renaming of method names. + ## 0.12.8+1 * Add `metadata` to `QuerySnapshot`. diff --git a/packages/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/cloudfirestore/CloudFirestorePlugin.java b/packages/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/cloudfirestore/CloudFirestorePlugin.java index 0c3496b547fe..e13697ab4c7d 100644 --- a/packages/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/cloudfirestore/CloudFirestorePlugin.java +++ b/packages/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/cloudfirestore/CloudFirestorePlugin.java @@ -31,6 +31,7 @@ import com.google.firebase.firestore.FirebaseFirestoreSettings; import com.google.firebase.firestore.GeoPoint; import com.google.firebase.firestore.ListenerRegistration; +import com.google.firebase.firestore.MetadataChanges; import com.google.firebase.firestore.Query; import com.google.firebase.firestore.QuerySnapshot; import com.google.firebase.firestore.SetOptions; @@ -624,22 +625,32 @@ public void run() { int handle = nextListenerHandle++; EventObserver observer = new EventObserver(handle); observers.put(handle, observer); - listenerRegistrations.put(handle, getQuery(arguments).addSnapshotListener(observer)); + MetadataChanges metadataChanges = + (Boolean) arguments.get("includeMetadataChanges") + ? MetadataChanges.INCLUDE + : MetadataChanges.EXCLUDE; + listenerRegistrations.put( + handle, getQuery(arguments).addSnapshotListener(metadataChanges, observer)); result.success(handle); break; } - case "Query#addDocumentListener": + case "DocumentReference#addSnapshotListener": { Map arguments = call.arguments(); int handle = nextListenerHandle++; DocumentObserver observer = new DocumentObserver(handle); documentObservers.put(handle, observer); + MetadataChanges metadataChanges = + (Boolean) arguments.get("includeMetadataChanges") + ? MetadataChanges.INCLUDE + : MetadataChanges.EXCLUDE; listenerRegistrations.put( - handle, getDocumentReference(arguments).addSnapshotListener(observer)); + handle, + getDocumentReference(arguments).addSnapshotListener(metadataChanges, observer)); result.success(handle); break; } - case "Query#removeListener": + case "removeListener": { Map arguments = call.arguments(); int handle = (Integer) arguments.get("handle"); diff --git a/packages/cloud_firestore/example/lib/main.dart b/packages/cloud_firestore/example/lib/main.dart index 27138f2fbd9e..30c1e2ce3036 100755 --- a/packages/cloud_firestore/example/lib/main.dart +++ b/packages/cloud_firestore/example/lib/main.dart @@ -41,8 +41,11 @@ class MessageList extends StatelessWidget { itemCount: messageCount, itemBuilder: (_, int index) { final DocumentSnapshot document = snapshot.data.documents[index]; + final dynamic message = document['message']; return ListTile( - title: Text(document['message'] ?? ''), + title: Text( + message != null ? message.toString() : '', + ), subtitle: Text('Message ${index + 1} of $messageCount'), ); }, @@ -54,7 +57,9 @@ class MessageList extends StatelessWidget { class MyHomePage extends StatelessWidget { MyHomePage({this.firestore}); + final Firestore firestore; + CollectionReference get messages => firestore.collection('messages'); Future _addMessage() async { diff --git a/packages/cloud_firestore/example/test_driver/cloud_firestore.dart b/packages/cloud_firestore/example/test_driver/cloud_firestore.dart index 5cad0386e6b8..2c984f7d6985 100644 --- a/packages/cloud_firestore/example/test_driver/cloud_firestore.dart +++ b/packages/cloud_firestore/example/test_driver/cloud_firestore.dart @@ -1,8 +1,9 @@ import 'dart:async'; -import 'package:flutter_driver/driver_extension.dart'; -import 'package:flutter_test/flutter_test.dart'; + import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter_driver/driver_extension.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { final Completer completer = Completer(); @@ -105,6 +106,36 @@ void main() { await ref.delete(); }); + test('includeMetadataChanges', () async { + final DocumentReference ref = firestore.collection('messages').document(); + final Stream snapshotWithoutMetadataChanges = + ref.snapshots(includeMetadataChanges: false).take(1); + final Stream snapshotsWithMetadataChanges = + ref.snapshots(includeMetadataChanges: true).take(3); + + ref.setData({'hello': 'world'}); + + final DocumentSnapshot snapshot = + await snapshotWithoutMetadataChanges.first; + expect(snapshot.metadata.hasPendingWrites, true); + expect(snapshot.metadata.isFromCache, true); + expect(snapshot.data['hello'], 'world'); + + final List snapshots = + await snapshotsWithMetadataChanges.toList(); + expect(snapshots[0].metadata.hasPendingWrites, true); + expect(snapshots[0].metadata.isFromCache, true); + expect(snapshots[0].data['hello'], 'world'); + expect(snapshots[1].metadata.hasPendingWrites, true); + expect(snapshots[1].metadata.isFromCache, false); + expect(snapshots[1].data['hello'], 'world'); + expect(snapshots[2].metadata.hasPendingWrites, false); + expect(snapshots[2].metadata.isFromCache, false); + expect(snapshots[2].data['hello'], 'world'); + + await ref.delete(); + }); + test('runTransaction', () async { final DocumentReference ref = firestore.collection('messages').document(); await ref.setData({ diff --git a/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m b/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m index 10ecc55a4335..71c4232af682 100644 --- a/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m +++ b/packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m @@ -526,39 +526,52 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result message:[exception name] details:[exception reason]]); } + NSNumber *includeMetadataChanges = call.arguments[@"includeMetadataChanges"]; id listener = [query - addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { - if (snapshot == nil) { - result(getFlutterError(error)); - return; - } - NSMutableDictionary *arguments = [parseQuerySnapshot(snapshot) mutableCopy]; - [arguments setObject:handle forKey:@"handle"]; - [weakSelf.channel invokeMethod:@"QuerySnapshot" arguments:arguments]; - }]; + addSnapshotListenerWithIncludeMetadataChanges:includeMetadataChanges.boolValue + listener:^(FIRQuerySnapshot *_Nullable snapshot, + NSError *_Nullable error) { + if (snapshot == nil) { + result(getFlutterError(error)); + return; + } + NSMutableDictionary *arguments = + [parseQuerySnapshot(snapshot) mutableCopy]; + [arguments setObject:handle forKey:@"handle"]; + [weakSelf.channel invokeMethod:@"QuerySnapshot" + arguments:arguments]; + }]; _listeners[handle] = listener; result(handle); - } else if ([@"Query#addDocumentListener" isEqualToString:call.method]) { + } else if ([@"DocumentReference#addSnapshotListener" isEqualToString:call.method]) { __block NSNumber *handle = [NSNumber numberWithInt:_nextListenerHandle++]; FIRDocumentReference *document = getDocumentReference(call.arguments); - id listener = - [document addSnapshotListener:^(FIRDocumentSnapshot *snapshot, NSError *_Nullable error) { - if (snapshot == nil) { - result(getFlutterError(error)); - return; - } - [weakSelf.channel invokeMethod:@"DocumentSnapshot" - arguments:@{ - @"handle" : handle, - @"path" : snapshot ? snapshot.reference.path : [NSNull null], - @"data" : snapshot.exists ? snapshot.data : [NSNull null], - @"metadata" : snapshot ? @{ - @"hasPendingWrites" : @(snapshot.metadata.hasPendingWrites), - @"isFromCache" : @(snapshot.metadata.isFromCache), - } - : [NSNull null], - }]; - }]; + NSNumber *includeMetadataChanges = call.arguments[@"includeMetadataChanges"]; + id listener = [document + addSnapshotListenerWithIncludeMetadataChanges:includeMetadataChanges.boolValue + listener:^(FIRDocumentSnapshot *snapshot, + NSError *_Nullable error) { + if (snapshot == nil) { + result(getFlutterError(error)); + return; + } + [weakSelf.channel + invokeMethod:@"DocumentSnapshot" + arguments:@{ + @"handle" : handle, + @"path" : snapshot ? snapshot.reference.path + : [NSNull null], + @"data" : snapshot.exists ? snapshot.data + : [NSNull null], + @"metadata" : snapshot ? @{ + @"hasPendingWrites" : + @(snapshot.metadata.hasPendingWrites), + @"isFromCache" : + @(snapshot.metadata.isFromCache), + } + : [NSNull null], + }]; + }]; _listeners[handle] = listener; result(handle); } else if ([@"Query#getDocuments" isEqualToString:call.method]) { @@ -581,7 +594,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } result(parseQuerySnapshot(snapshot)); }]; - } else if ([@"Query#removeListener" isEqualToString:call.method]) { + } else if ([@"removeListener" isEqualToString:call.method]) { NSNumber *handle = call.arguments[@"handle"]; [[_listeners objectForKey:handle] remove]; [_listeners removeObjectForKey:handle]; diff --git a/packages/cloud_firestore/lib/src/document_reference.dart b/packages/cloud_firestore/lib/src/document_reference.dart index 2edf8ac548ad..413cf41be0be 100644 --- a/packages/cloud_firestore/lib/src/document_reference.dart +++ b/packages/cloud_firestore/lib/src/document_reference.dart @@ -116,7 +116,8 @@ class DocumentReference { /// Notifies of documents at this location // TODO(jackson): Reduce code duplication with [Query] - Stream snapshots() { + Stream snapshots({bool includeMetadataChanges = false}) { + assert(includeMetadataChanges != null); Future _handle; // It's fine to let the StreamController be garbage collected once all the // subscribers have cancelled; this analyzer warning is safe to ignore. @@ -124,10 +125,11 @@ class DocumentReference { controller = StreamController.broadcast( onListen: () { _handle = Firestore.channel.invokeMethod( - 'Query#addDocumentListener', + 'DocumentReference#addSnapshotListener', { 'app': firestore.app.name, 'path': path, + 'includeMetadataChanges': includeMetadataChanges, }, ).then((dynamic result) => result); _handle.then((int handle) { @@ -137,7 +139,7 @@ class DocumentReference { onCancel: () { _handle.then((int handle) async { await Firestore.channel.invokeMethod( - 'Query#removeListener', + 'removeListener', {'handle': handle}, ); Firestore._documentObservers.remove(handle); diff --git a/packages/cloud_firestore/lib/src/query.dart b/packages/cloud_firestore/lib/src/query.dart index e1f6e2aa5a96..114b48a6df57 100644 --- a/packages/cloud_firestore/lib/src/query.dart +++ b/packages/cloud_firestore/lib/src/query.dart @@ -50,7 +50,8 @@ class Query { /// Notifies of query results at this location // TODO(jackson): Reduce code duplication with [DocumentReference] - Stream snapshots() { + Stream snapshots({bool includeMetadataChanges = false}) { + assert(includeMetadataChanges != null); Future _handle; // It's fine to let the StreamController be garbage collected once all the // subscribers have cancelled; this analyzer warning is safe to ignore. @@ -64,6 +65,7 @@ class Query { 'path': _path, 'isCollectionGroup': _isCollectionGroup, 'parameters': _parameters, + 'includeMetadataChanges': includeMetadataChanges, }, ).then((dynamic result) => result); _handle.then((int handle) { @@ -73,7 +75,7 @@ class Query { onCancel: () { _handle.then((int handle) async { await Firestore.channel.invokeMethod( - 'Query#removeListener', + 'removeListener', {'handle': handle}, ); Firestore._queryObservers.remove(handle); diff --git a/packages/cloud_firestore/pubspec.yaml b/packages/cloud_firestore/pubspec.yaml index 1c43c3fd018e..475130fbb5f5 100755 --- a/packages/cloud_firestore/pubspec.yaml +++ b/packages/cloud_firestore/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Cloud Firestore, a cloud-hosted, noSQL database live synchronization and offline support on Android and iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/cloud_firestore -version: 0.12.8+1 +version: 0.12.9 flutter: plugin: diff --git a/packages/cloud_firestore/test/cloud_firestore_test.dart b/packages/cloud_firestore/test/cloud_firestore_test.dart index 92173085c24c..c68f04405009 100755 --- a/packages/cloud_firestore/test/cloud_firestore_test.dart +++ b/packages/cloud_firestore/test/cloud_firestore_test.dart @@ -79,7 +79,7 @@ void main() { ); }); return handle; - case 'Query#addDocumentListener': + case 'DocumentReference#addSnapshotListener': final int handle = mockHandleId++; // Wait before sending a message back. // Otherwise the first request didn't have the time to finish. @@ -330,8 +330,9 @@ void main() { expect(collectionReference.path, equals('foo')); }); test('listen', () async { - final QuerySnapshot snapshot = - await collectionReference.snapshots().first; + final QuerySnapshot snapshot = await collectionReference + .snapshots(includeMetadataChanges: true) + .first; final DocumentSnapshot document = snapshot.documents[0]; expect(document.documentID, equals('0')); expect(document.reference.path, equals('foo/0')); @@ -348,11 +349,12 @@ void main() { 'parameters': { 'where': >[], 'orderBy': >[], - } + }, + 'includeMetadataChanges': true, }, ), isMethodCall( - 'Query#removeListener', + 'removeListener', arguments: {'handle': 0}, ), ]); @@ -379,11 +381,12 @@ void main() { ['createdAt', '<', 100], ], 'orderBy': >[], - } + }, + 'includeMetadataChanges': false, }, ), isMethodCall( - 'Query#removeListener', + 'removeListener', arguments: {'handle': 0}, ), ]), @@ -411,11 +414,12 @@ void main() { ['profile', '==', null], ], 'orderBy': >[], - } + }, + 'includeMetadataChanges': false, }, ), isMethodCall( - 'Query#removeListener', + 'removeListener', arguments: {'handle': 0}, ), ]), @@ -443,11 +447,12 @@ void main() { 'orderBy': >[ ['createdAt', false] ], - } + }, + 'includeMetadataChanges': false, }, ), isMethodCall( - 'Query#removeListener', + 'removeListener', arguments: {'handle': 0}, ), ]), @@ -457,8 +462,10 @@ void main() { group('DocumentReference', () { test('listen', () async { - final DocumentSnapshot snapshot = - await firestore.document('path/to/foo').snapshots().first; + final DocumentSnapshot snapshot = await firestore + .document('path/to/foo') + .snapshots(includeMetadataChanges: true) + .first; expect(snapshot.documentID, equals('foo')); expect(snapshot.reference.path, equals('path/to/foo')); expect(snapshot.data, equals(kMockDocumentSnapshotData)); @@ -468,14 +475,15 @@ void main() { log, [ isMethodCall( - 'Query#addDocumentListener', + 'DocumentReference#addSnapshotListener', arguments: { 'app': app.name, 'path': 'path/to/foo', + 'includeMetadataChanges': true, }, ), isMethodCall( - 'Query#removeListener', + 'removeListener', arguments: {'handle': 0}, ), ],