Skip to content

Commit

Permalink
[cloud_firestore] add support for MetadataChanges (#1918)
Browse files Browse the repository at this point in the history
* [cloud_firestore] add support for MetadataChanges

* test: add integration test for metadata changes
  • Loading branch information
long1eu authored and collinjackson committed Jul 29, 2019
1 parent d989f5b commit d05ec30
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 57 deletions.
7 changes: 7 additions & 0 deletions packages/cloud_firestore/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, Object> 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<String, Object> arguments = call.arguments();
int handle = (Integer) arguments.get("handle");
Expand Down
7 changes: 6 additions & 1 deletion packages/cloud_firestore/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'] ?? '<No message retrieved>'),
title: Text(
message != null ? message.toString() : '<No message retrieved>',
),
subtitle: Text('Message ${index + 1} of $messageCount'),
);
},
Expand All @@ -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<void> _addMessage() async {
Expand Down
35 changes: 33 additions & 2 deletions packages/cloud_firestore/example/test_driver/cloud_firestore.dart
Original file line number Diff line number Diff line change
@@ -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<String> completer = Completer<String>();
Expand Down Expand Up @@ -105,6 +106,36 @@ void main() {
await ref.delete();
});

test('includeMetadataChanges', () async {
final DocumentReference ref = firestore.collection('messages').document();
final Stream<DocumentSnapshot> snapshotWithoutMetadataChanges =
ref.snapshots(includeMetadataChanges: false).take(1);
final Stream<DocumentSnapshot> snapshotsWithMetadataChanges =
ref.snapshots(includeMetadataChanges: true).take(3);

ref.setData(<String, dynamic>{'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<DocumentSnapshot> 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(<String, dynamic>{
Expand Down
71 changes: 42 additions & 29 deletions packages/cloud_firestore/ios/Classes/CloudFirestorePlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -526,39 +526,52 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result
message:[exception name]
details:[exception reason]]);
}
NSNumber *includeMetadataChanges = call.arguments[@"includeMetadataChanges"];
id<FIRListenerRegistration> 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<FIRListenerRegistration> 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<FIRListenerRegistration> 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]) {
Expand All @@ -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];
Expand Down
8 changes: 5 additions & 3 deletions packages/cloud_firestore/lib/src/document_reference.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,18 +116,20 @@ class DocumentReference {

/// Notifies of documents at this location
// TODO(jackson): Reduce code duplication with [Query]
Stream<DocumentSnapshot> snapshots() {
Stream<DocumentSnapshot> snapshots({bool includeMetadataChanges = false}) {
assert(includeMetadataChanges != null);
Future<int> _handle;
// It's fine to let the StreamController be garbage collected once all the
// subscribers have cancelled; this analyzer warning is safe to ignore.
StreamController<DocumentSnapshot> controller; // ignore: close_sinks
controller = StreamController<DocumentSnapshot>.broadcast(
onListen: () {
_handle = Firestore.channel.invokeMethod<int>(
'Query#addDocumentListener',
'DocumentReference#addSnapshotListener',
<String, dynamic>{
'app': firestore.app.name,
'path': path,
'includeMetadataChanges': includeMetadataChanges,
},
).then<int>((dynamic result) => result);
_handle.then((int handle) {
Expand All @@ -137,7 +139,7 @@ class DocumentReference {
onCancel: () {
_handle.then((int handle) async {
await Firestore.channel.invokeMethod<void>(
'Query#removeListener',
'removeListener',
<String, dynamic>{'handle': handle},
);
Firestore._documentObservers.remove(handle);
Expand Down
6 changes: 4 additions & 2 deletions packages/cloud_firestore/lib/src/query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ class Query {

/// Notifies of query results at this location
// TODO(jackson): Reduce code duplication with [DocumentReference]
Stream<QuerySnapshot> snapshots() {
Stream<QuerySnapshot> snapshots({bool includeMetadataChanges = false}) {
assert(includeMetadataChanges != null);
Future<int> _handle;
// It's fine to let the StreamController be garbage collected once all the
// subscribers have cancelled; this analyzer warning is safe to ignore.
Expand All @@ -64,6 +65,7 @@ class Query {
'path': _path,
'isCollectionGroup': _isCollectionGroup,
'parameters': _parameters,
'includeMetadataChanges': includeMetadataChanges,
},
).then<int>((dynamic result) => result);
_handle.then((int handle) {
Expand All @@ -73,7 +75,7 @@ class Query {
onCancel: () {
_handle.then((int handle) async {
await Firestore.channel.invokeMethod<void>(
'Query#removeListener',
'removeListener',
<String, dynamic>{'handle': handle},
);
Firestore._queryObservers.remove(handle);
Expand Down
2 changes: 1 addition & 1 deletion packages/cloud_firestore/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <flutter-dev@googlegroups.com>
homepage: https://github.com/flutter/plugins/tree/master/packages/cloud_firestore
version: 0.12.8+1
version: 0.12.9

flutter:
plugin:
Expand Down
38 changes: 23 additions & 15 deletions packages/cloud_firestore/test/cloud_firestore_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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'));
Expand All @@ -348,11 +349,12 @@ void main() {
'parameters': <String, dynamic>{
'where': <List<dynamic>>[],
'orderBy': <List<dynamic>>[],
}
},
'includeMetadataChanges': true,
},
),
isMethodCall(
'Query#removeListener',
'removeListener',
arguments: <String, dynamic>{'handle': 0},
),
]);
Expand All @@ -379,11 +381,12 @@ void main() {
<dynamic>['createdAt', '<', 100],
],
'orderBy': <List<dynamic>>[],
}
},
'includeMetadataChanges': false,
},
),
isMethodCall(
'Query#removeListener',
'removeListener',
arguments: <String, dynamic>{'handle': 0},
),
]),
Expand Down Expand Up @@ -411,11 +414,12 @@ void main() {
<dynamic>['profile', '==', null],
],
'orderBy': <List<dynamic>>[],
}
},
'includeMetadataChanges': false,
},
),
isMethodCall(
'Query#removeListener',
'removeListener',
arguments: <String, dynamic>{'handle': 0},
),
]),
Expand Down Expand Up @@ -443,11 +447,12 @@ void main() {
'orderBy': <List<dynamic>>[
<dynamic>['createdAt', false]
],
}
},
'includeMetadataChanges': false,
},
),
isMethodCall(
'Query#removeListener',
'removeListener',
arguments: <String, dynamic>{'handle': 0},
),
]),
Expand All @@ -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));
Expand All @@ -468,14 +475,15 @@ void main() {
log,
<Matcher>[
isMethodCall(
'Query#addDocumentListener',
'DocumentReference#addSnapshotListener',
arguments: <String, dynamic>{
'app': app.name,
'path': 'path/to/foo',
'includeMetadataChanges': true,
},
),
isMethodCall(
'Query#removeListener',
'removeListener',
arguments: <String, dynamic>{'handle': 0},
),
],
Expand Down

0 comments on commit d05ec30

Please sign in to comment.