From 51101c9c1d064b35964f9fb08c9c370901957557 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 3 Dec 2019 13:10:26 -0800 Subject: [PATCH 01/16] Package scaffolding and initial docs. --- .../CHANGELOG.md | 3 +++ .../LICENSE | 27 +++++++++++++++++++ .../README.md | 26 ++++++++++++++++++ .../pubspec.yaml | 22 +++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 packages/cloud_firestore/cloud_firestore_platform_interface/CHANGELOG.md create mode 100644 packages/cloud_firestore/cloud_firestore_platform_interface/LICENSE create mode 100644 packages/cloud_firestore/cloud_firestore_platform_interface/README.md create mode 100644 packages/cloud_firestore/cloud_firestore_platform_interface/pubspec.yaml diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/CHANGELOG.md b/packages/cloud_firestore/cloud_firestore_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..6fadda91b380 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial open-source release. diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/LICENSE b/packages/cloud_firestore/cloud_firestore_platform_interface/LICENSE new file mode 100644 index 000000000000..000b4618d2bd --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/README.md b/packages/cloud_firestore/cloud_firestore_platform_interface/README.md new file mode 100644 index 000000000000..f354005e41c6 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/README.md @@ -0,0 +1,26 @@ +# cloud_firestore_platform_interface + +A common platform interface for the [`cloud_firestore`][1] plugin. + +This interface allows platform-specific implementations of the `cloud_firestore` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `cloud_firestore`, extend +[`CloudFirestorePlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`CloudFirestorePlatform` by calling +`CloudFirestorePlatform.instance = MyCloudFirestorePlatform()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../cloud_firestore +[2]: lib/cloud_firestore_platform_interface.dart diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/pubspec.yaml b/packages/cloud_firestore/cloud_firestore_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..7aa80cb809a1 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/pubspec.yaml @@ -0,0 +1,22 @@ +name: cloud_firestore_platform_interface +description: A common platform interface for the cloud_firestore plugin. +author: Flutter Team +homepage: https://github.com/FirebaseExtended/flutterfire/tree/master/packages/cloud_firestore/cloud_firestore_platform_interface +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 1.0.0 + +dependencies: + flutter: + sdk: flutter + meta: ^1.0.5 + quiver: ">=2.0.0 <3.0.0" + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^4.1.1 + +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.9.1+hotfix.5 <2.0.0" From bbf60ed4ea67b896c930c6769ab07f2219adce19 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 4 Dec 2019 17:18:30 -0800 Subject: [PATCH 02/16] cloud_firestore_platform_interface [wip] --- .../cloud_firestore_platform_interface.dart | 230 ++++++++++++ .../src/method_channel_cloud_firestore.dart | 343 ++++++++++++++++++ ...oud_firestore_platform_interface_test.dart | 39 ++ .../method_channel_firebase_core_test.dart | 21 ++ 4 files changed, 633 insertions(+) create mode 100644 packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart create mode 100644 packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart create mode 100644 packages/cloud_firestore/cloud_firestore_platform_interface/test/cloud_firestore_platform_interface_test.dart create mode 100644 packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_firebase_core_test.dart diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart new file mode 100644 index 000000000000..772749c91e87 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart @@ -0,0 +1,230 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:meta/meta.dart' show required, visibleForTesting; + +import 'src/method_channel_cloud_firestore.dart'; + +/// The interface that implementations of `cloud_firestore` must extend. +/// +/// Platform implementations should extend this class rather than implement it +/// as `cloud_firestore` does not consider newly added methods to be breaking +/// changes. Extending this class (using `extends`) ensures that the subclass +/// will get the default implementation, while platform implementations that +/// `implements` this interface will be broken by newly added +/// [CloudFirestorePlatform] methods. +abstract class CloudFirestorePlatform { + /// Only mock implementations should set this to `true`. + /// + /// Mockito mocks implement this class with `implements` which is forbidden + /// (see class docs). This property provides a backdoor for mocks to skip the + /// verification that the class isn't implemented with `implements`. + @visibleForTesting + bool get isMock => false; + + /// The default instance of [CloudFirestorePlatform] to use. + /// + /// Platform-specific plugins should override this with their own class + /// that extends [CloudFirestorePlatform] when they register themselves. + /// + /// Defaults to [MethodChannelCloudFirestore]. + static CloudFirestorePlatform get instance => _instance; + + static CloudFirestorePlatform _instance = MethodChannelCloudFirestore(); + + // TODO(amirh): Extract common platform interface logic. + // https://github.com/flutter/flutter/issues/43368 + static set instance(CloudFirestorePlatform instance) { + if (!instance.isMock) { + try { + instance._verifyProvidesDefaultImplementations(); + } on NoSuchMethodError catch (_) { + throw AssertionError( + 'Platform interfaces must not be implemented with `implements`'); + } + } + _instance = instance; + } + + /// This method ensures that [CloudFirestorePlatform] isn't implemented with `implements`. + /// + /// See class docs for more details on why using `implements` to implement + /// [CloudFirestorePlatform] is forbidden. + /// + /// This private method is called by the [instance] setter, which should fail + /// if the provided instance is a class implemented with `implements`. + void _verifyProvidesDefaultImplementations() {} + + // Actual API + // Platform calls + Future onQuerySnapshot(PlatformQuerySnapshot snapshot) async { + throw UnimplementedError('onQuerySnapshot() is not implemented'); + } + + Future onDocumentSnapshot(PlatformDocumentSnapshot snapshot) async { + throw UnimplementedError('onDocumentSnapshot() is not implemented'); + } + + Future onDoTransaction(PlatformTransaction transaction) async { + throw UnimplementedError('onDoTransaction() is not implemented'); + } + + // Global + /// Removes any listener by its handle. + /// All handles must be unique across al types of listeners. + Future removeListener(int handle) async { + throw UnimplementedError('removeListener() is not implemented'); + } + + // Firestore + Future enablePersistence(String app, {@required bool enable}) async { + throw UnimplementedError('enablePersistence() is not implemented'); + } + + Future settings(String app, { + bool persistenceEnabled, + String host, + bool sslEnabled, + bool timestampsInSnapshotsEnabled, + int cacheSizeBytes, + }) async { + throw UnimplementedError('settings() is not implemented'); + } + + Future> runTransaction(String app, { + @required int transactionId, + int transactionTimeout, + }) async { + throw UnimplementedError('runTransaction() is not implemented'); + } + + // Document Reference + Future setDocumentReferenceData(String app, { + @required String path, + Map data, + // TODO: Type https://firebase.google.com/docs/reference/js/firebase.firestore.SetOptions.html + Map options, + }) async { + throw UnimplementedError('setDocumentReferenceData() is not implemented'); + } + + Future updateDocumentReferenceData(String app, { + @required String path, + Map data, + }) async { + throw UnimplementedError('updateDocumentReferenceData() is not implemented'); + } + + // TODO: Type this return + Future> getDocumentReference(String app, { + @required String path, + @required String source, + }) async { + throw UnimplementedError('getDocumentReference() is not implemented'); + } + + Future deleteDocumentReference(String app, { + @required String path, + }) async { + throw UnimplementedError('deleteDocumentReference() is not implemented'); + } + + // TODO: Port to stream + Future addDocumentReferenceSnapshotListener(String app, { + @required String path, + bool includeMetadataChanges, + }) async { + throw UnimplementedError('addDocumentReferenceSnapshotListener() is not implemented'); + } + + // Query + // TODO: Port to stream + Future addQuerySnapshotListener(String app, { + @required String path, + bool isCollectionGroup, + Map parameters, + bool includeMetadataChanges, + }) async { + throw UnimplementedError('addQuerySnapshotListener() is not implemented'); + } + + //TODO: Type this return + Future> getQueryDocuments(String app, { + @required String path, + bool isCollectionGroup, + Map parameters, + String source, + }) async { + throw UnimplementedError('getQueryDocuments() is not implemented'); + } + + // Transaction + // TODO: Type this return + Future> getTransaction(String app, { + @required String path, + @required int transactionId, + }) async { + throw UnimplementedError('getTransaction() is not implemented'); + } + + Future deleteTransaction(String app, { + @required String path, + @required int transactionId, + }) async { + throw UnimplementedError('deleteTransaction() is not implemented'); + } + + Future updateTransaction(String app, { + @required String path, + @required int transactionId, + Map data, + }) async { + throw UnimplementedError('updateTransaction() is not implemented'); + } + + Future setTransaction(String app, { + @required String path, + @required int transactionId, + Map data, + }) async { + throw UnimplementedError('setTransaction() is not implemented'); + } + + // Write Batch + Future createWriteBatch(String app) async { + throw UnimplementedError('createWriteBatch() is not implemented'); + } + + Future commitWriteBatch({ + @required dynamic handle, + }) async { + throw UnimplementedError('commitWriteBatch() is not implemented'); + } + + Future deleteWriteBatch(String app, { + @required dynamic handle, + @required String path, + }) async { + throw UnimplementedError('deleteWriteBatch() is not implemented'); + } + + Future setWriteBatchData(String app, { + @required dynamic handle, + @required String path, + Map data, + Map options, + }) async { + throw UnimplementedError('setWriteBatchData() is not implemented'); + } + + Future updateWriteBatchData(String app, { + @required dynamic handle, + @required String path, + Map data, + }) async { + throw UnimplementedError('updateWriteBatchData() is not implemented'); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart new file mode 100644 index 000000000000..14043b45b2b6 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart @@ -0,0 +1,343 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show visibleForTesting, required; + +import '../cloud_firestore_platform_interface.dart'; + +class MethodChannelCloudFirestore extends CloudFirestorePlatform { + MethodChannelCloudFirestore() { + channel.setMethodCallHandler((MethodCall call) async { + if (call.method == 'QuerySnapshot') { + final QuerySnapshot snapshot = QuerySnapshot._(call.arguments, this); + _queryObservers[call.arguments['handle']].add(snapshot); + } else if (call.method == 'DocumentSnapshot') { + final DocumentSnapshot snapshot = DocumentSnapshot._( + call.arguments['path'], + _asStringKeyedMap(call.arguments['data']), + SnapshotMetadata._(call.arguments['metadata']['hasPendingWrites'], + call.arguments['metadata']['isFromCache']), + this, + ); + _documentObservers[call.arguments['handle']].add(snapshot); + } else if (call.method == 'DoTransaction') { + final int transactionId = call.arguments['transactionId']; + final Transaction transaction = Transaction(transactionId, this); + final dynamic result = + await _transactionHandlers[transactionId](transaction); + await transaction._finish(); + return result; + } + }); + } + + @visibleForTesting + static const MethodChannel channel = MethodChannel( + 'plugins.flutter.io/cloud_firestore', + ); + + // Platform calls + Future _handlePlatformCall(MethodCall call) async { + switch(call.method) { + case 'QuerySnapshot': + final QuerySnapshot snapshot = QuerySnapshot._(call.arguments, this); + _queryObservers[call.arguments['handle']].add(snapshot); + break; + case 'DocumentSnapshot': + final DocumentSnapshot snapshot = DocumentSnapshot._( + call.arguments['path'], + _asStringKeyedMap(call.arguments['data']), + SnapshotMetadata._(call.arguments['metadata']['hasPendingWrites'], + call.arguments['metadata']['isFromCache']), + this, + ); + _documentObservers[call.arguments['handle']].add(snapshot); + break; + case 'DoTransaction': + final int transactionId = call.arguments['transactionId']; + final Transaction transaction = Transaction(transactionId, this); + final dynamic result = + await _transactionHandlers[transactionId](transaction); + await transaction._finish(); + return result; + break; + } + } + + @override + Future onQuerySnapshot(PlatformQuerySnapshot snapshot) { + + } + + @override + Future onDocumentSnapshot(PlatformDocumentSnapshot snapshot) { + + } + + @override + Future onDoTransaction(PlatformTransaction transaction) { + + } + + // Global + @override + Future removeListener(int handle) => + channel.invokeMethod( + 'removeListener', + {'handle': handle}, + ); + + + // Firestore + @override + Future enablePersistence(String app, {bool enable = true}) => + channel + .invokeMethod('Firestore#enablePersistence', { + 'app': app, + 'enable': enable, + }); + + + @override + Future settings(String app, { + bool persistenceEnabled, + String host, + bool sslEnabled, + bool timestampsInSnapshotsEnabled, + int cacheSizeBytes, + }) => + channel.invokeMethod('Firestore#settings', { + 'app': app, + 'persistenceEnabled': persistenceEnabled, + 'host': host, + 'sslEnabled': sslEnabled, + 'timestampsInSnapshotsEnabled': timestampsInSnapshotsEnabled, + 'cacheSizeBytes': cacheSizeBytes, + }); + + + @override + Future> runTransaction(String app, { + @required int transactionId, + int transactionTimeout, + }) => channel + .invokeMapMethod( + 'Firestore#runTransaction', { + 'app': app, + 'transactionId': transactionId, + 'transactionTimeout': transactionTimeout + }); + + // Document Reference + @override + Future setDocumentReferenceData(String app, { + @required String path, + // TODO: Type SetOptions: https://firebase.google.com/docs/reference/js/firebase.firestore.SetOptions.html + Map options, + Map data = const {}, + }) => channel.invokeMethod( + 'DocumentReference#setData', + { + 'app': app, + 'path': path, + 'data': data, + 'options': options, + }, + ); + + @override + Future updateDocumentReferenceData(String app, { + @required String path, + Map data = const {}, + }) => channel.invokeMethod( + 'DocumentReference#updateData', + { + 'app': app, + 'path': path, + 'data': data, + }, + ); + + // TODO: Type this return + @override + Future> getDocumentReference(String app, { + @required String path, + @required String source, + }) => channel.invokeMapMethod( + 'DocumentReference#get', + { + 'app': app, + 'path': path, + 'source': source, + }, + ); + + @override + Future deleteDocumentReference(String app, { + @required String path, + }) => channel.invokeMethod( + 'DocumentReference#delete', + {'app': app, 'path': path}, + ); + + // TODO: Port to stream + @override + Future addDocumentReferenceSnapshotListener(String app, { + @required String path, + bool includeMetadataChanges, + }) => channel.invokeMethod( + 'DocumentReference#addSnapshotListener', + { + 'app': app, + 'path': path, + 'includeMetadataChanges': includeMetadataChanges, + }, + ); + + // Query + // TODO: Port to stream + @override + Future addQuerySnapshotListener(String app, { + @required String path, + bool isCollectionGroup, + Map parameters, + bool includeMetadataChanges, + }) => channel.invokeMethod( + 'Query#addSnapshotListener', + { + 'app': app, + 'path': path, + 'isCollectionGroup': isCollectionGroup, + 'parameters': parameters, + 'includeMetadataChanges': includeMetadataChanges, + }, + ); + + //TODO: Type this return + @override + Future> getQueryDocuments(String app, { + @required String path, + bool isCollectionGroup, + Map parameters, + String source, + }) => channel.invokeMapMethod( + 'Query#getDocuments', + { + 'app': app, + 'path': path, + 'isCollectionGroup': isCollectionGroup, + 'parameters': parameters, + 'source': source, + }, + ); + + // Transaction + // TODO: Type this return + @override + Future> getTransaction(String app, { + @required String path, + @required int transactionId, + }) => channel + .invokeMapMethod('Transaction#get', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + }); + + @override + Future deleteTransaction(String app, { + @required String path, + @required int transactionId, + }) => channel + .invokeMethod('Transaction#delete', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + }); + + @override + Future updateTransaction(String app, { + @required String path, + @required int transactionId, + Map data, + }) => channel + .invokeMethod('Transaction#update', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + 'data': data, + }); + + @override + Future setTransaction(String app, { + @required String path, + @required int transactionId, + Map data, + }) => channel + .invokeMethod('Transaction#set', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + 'data': data, + }); + + // Write Batch + @override + Future createWriteBatch(String app) => channel.invokeMethod( + 'WriteBatch#create', {'app': app}); + + @override + Future commitWriteBatch({ + @required dynamic handle, + }) => channel.invokeMethod( + 'WriteBatch#commit', {'handle': handle}); + + @override + Future deleteWriteBatch(String app, { + @required dynamic handle, + @required String path, + }) => channel.invokeMethod( + 'WriteBatch#delete', + { + 'app': app, + 'handle': handle, + 'path': path, + }, + ); + + @override + Future setWriteBatchData(String app, { + @required dynamic handle, + @required String path, + Map data, + Map options, + }) => channel.invokeMethod( + 'WriteBatch#setData', + { + 'app': app, + 'handle': handle, + 'path': path, + 'data': data, + 'options': options, + }, + ); + + @override + Future updateWriteBatchData(String app, { + @required dynamic handle, + @required String path, + Map data, + }) => channel.invokeMethod( + 'WriteBatch#updateData', + { + 'app': app, + 'handle': handle, + 'path': path, + 'data': data, + }, + ); +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/test/cloud_firestore_platform_interface_test.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/test/cloud_firestore_platform_interface_test.dart new file mode 100644 index 000000000000..48602f1f88fe --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/test/cloud_firestore_platform_interface_test.dart @@ -0,0 +1,39 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; +import 'package:cloud_firestore_platform_interface/src/method_channel_cloud_firestore.dart'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +void main() { + group('$CloudFirestorePlatform', () { + test('$MethodChannelCloudFirestore is the default instance', () { + expect(CloudFirestorePlatform.instance, isA()); + }); + + test('Cannot be implemented with `implements`', () { + expect(() { + CloudFirestorePlatform.instance = ImplementsCloudFirestorePlatform(); + }, throwsAssertionError); + }); + + test('Can be extended', () { + CloudFirestorePlatform.instance = ExtendsCloudFirestorePlatform(); + }); + + test('Can be mocked with `implements`', () { + final ImplementsCloudFirestorePlatform mock = + ImplementsCloudFirestorePlatform(); + when(mock.isMock).thenReturn(true); + CloudFirestorePlatform.instance = mock; + }); + }); +} + +class ImplementsCloudFirestorePlatform extends Mock + implements CloudFirestorePlatform {} + +class ExtendsCloudFirestorePlatform extends CloudFirestorePlatform {} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_firebase_core_test.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_firebase_core_test.dart new file mode 100644 index 000000000000..d781a33c3cdc --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_firebase_core_test.dart @@ -0,0 +1,21 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; +import 'package:cloud_firestore_platform_interface/src/method_channel_cloud_firestore.dart'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$MethodChannelCloudFirestore', () { + final MethodChannelCloudFirestore channelPlatform = + MethodChannelCloudFirestore(); + + + + }); +} From 4254aef41fa1bcf4567a3eefc164725047949dc8 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 4 Dec 2019 17:18:59 -0800 Subject: [PATCH 03/16] [cloud_firestore] Use the platform interface --- .../example/.flutter-plugins-dependencies | 1 + .../cloud_firestore/lib/cloud_firestore.dart | 1 + .../lib/src/document_reference.dart | 49 +++--------------- .../cloud_firestore/lib/src/firestore.dart | 51 +++---------------- .../cloud_firestore/lib/src/query.dart | 28 ++-------- .../cloud_firestore/lib/src/transaction.dart | 31 ++--------- .../cloud_firestore/lib/src/write_batch.dart | 36 ++----------- .../cloud_firestore/pubspec.yaml | 2 + .../cloud_firestore_platform_interface.dart | 1 - .../src/method_channel_cloud_firestore.dart | 2 - 10 files changed, 32 insertions(+), 170 deletions(-) create mode 100644 packages/cloud_firestore/cloud_firestore/example/.flutter-plugins-dependencies diff --git a/packages/cloud_firestore/cloud_firestore/example/.flutter-plugins-dependencies b/packages/cloud_firestore/cloud_firestore/example/.flutter-plugins-dependencies new file mode 100644 index 000000000000..7b5ad896f47b --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/example/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"cloud_firestore","dependencies":["firebase_core"]},{"name":"firebase_core","dependencies":[]}]} \ No newline at end of file diff --git a/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart index aef386e77d26..fce55bfdc3fe 100755 --- a/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart @@ -9,6 +9,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'dart:ui' show hashValues, hashList; +import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; import 'package:collection/collection.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart b/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart index 413cf41be0be..e8b1ca676fd5 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart @@ -48,15 +48,7 @@ class DocumentReference { /// If [merge] is true, the provided data will be merged into an /// existing document instead of overwriting. Future setData(Map data, {bool merge = false}) { - return Firestore.channel.invokeMethod( - 'DocumentReference#setData', - { - 'app': firestore.app.name, - 'path': path, - 'data': data, - 'options': {'merge': merge}, - }, - ); + return Firestore.platform.setDocumentReferenceData(firestore.app.name, path: path, data: data, options: {'merge': merge},); } /// Updates fields in the document referred to by this [DocumentReference]. @@ -66,14 +58,7 @@ class DocumentReference { /// /// If no document exists yet, the update will fail. Future updateData(Map data) { - return Firestore.channel.invokeMethod( - 'DocumentReference#updateData', - { - 'app': firestore.app.name, - 'path': path, - 'data': data, - }, - ); + return Firestore.platform.updateDocumentReferenceData(firestore.app.name, path: path, data: data,); } /// Reads the document referenced by this [DocumentReference]. @@ -81,14 +66,8 @@ class DocumentReference { /// If no document exists, the read will return null. Future get({Source source = Source.serverAndCache}) async { final Map data = - await Firestore.channel.invokeMapMethod( - 'DocumentReference#get', - { - 'app': firestore.app.name, - 'path': path, - 'source': _getSourceString(source), - }, - ); + await Firestore.platform.getDocumentReference(firestore.app.name, path: path, source: _getSourceString(source),); + return DocumentSnapshot._( data['path'], _asStringKeyedMap(data['data']), @@ -100,10 +79,7 @@ class DocumentReference { /// Deletes the document referred to by this [DocumentReference]. Future delete() { - return Firestore.channel.invokeMethod( - 'DocumentReference#delete', - {'app': firestore.app.name, 'path': path}, - ); + return Firestore.platform.deleteDocumentReference(firestore.app.name, path: path,); } /// Returns the reference of a collection contained inside of this @@ -124,24 +100,15 @@ class DocumentReference { StreamController controller; // ignore: close_sinks controller = StreamController.broadcast( onListen: () { - _handle = Firestore.channel.invokeMethod( - 'DocumentReference#addSnapshotListener', - { - 'app': firestore.app.name, - 'path': path, - 'includeMetadataChanges': includeMetadataChanges, - }, - ).then((dynamic result) => result); + _handle = Firestore.platform.addDocumentReferenceSnapshotListener(firestore.app.name, path: path, includeMetadataChanges: includeMetadataChanges,) + .then((dynamic result) => result); _handle.then((int handle) { Firestore._documentObservers[handle] = controller; }); }, onCancel: () { _handle.then((int handle) async { - await Firestore.channel.invokeMethod( - 'removeListener', - {'handle': handle}, - ); + await Firestore.platform.removeListener(handle); Firestore._documentObservers.remove(handle); }); }, diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart index 5c164ed1e122..32e236383a0f 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart @@ -10,28 +10,6 @@ part of cloud_firestore; class Firestore { Firestore({FirebaseApp app}) : app = app ?? FirebaseApp.instance { if (_initialized) return; - channel.setMethodCallHandler((MethodCall call) async { - if (call.method == 'QuerySnapshot') { - final QuerySnapshot snapshot = QuerySnapshot._(call.arguments, this); - _queryObservers[call.arguments['handle']].add(snapshot); - } else if (call.method == 'DocumentSnapshot') { - final DocumentSnapshot snapshot = DocumentSnapshot._( - call.arguments['path'], - _asStringKeyedMap(call.arguments['data']), - SnapshotMetadata._(call.arguments['metadata']['hasPendingWrites'], - call.arguments['metadata']['isFromCache']), - this, - ); - _documentObservers[call.arguments['handle']].add(snapshot); - } else if (call.method == 'DoTransaction') { - final int transactionId = call.arguments['transactionId']; - final Transaction transaction = Transaction(transactionId, this); - final dynamic result = - await _transactionHandlers[transactionId](transaction); - await transaction._finish(); - return result; - } - }); _initialized = true; } @@ -46,10 +24,7 @@ class Firestore { static bool _initialized = false; @visibleForTesting - static const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/cloud_firestore', - StandardMethodCodec(FirestoreMessageCodec()), - ); + static CloudFirestorePlatform platform = CloudFirestorePlatform.instance; static final Map> _queryObservers = >{}; @@ -125,24 +100,14 @@ class Firestore { 'Transaction timeout must be more than 0 milliseconds'); final int transactionId = _transactionHandlerId++; _transactionHandlers[transactionId] = transactionHandler; - final Map result = await channel - .invokeMapMethod( - 'Firestore#runTransaction', { - 'app': app.name, - 'transactionId': transactionId, - 'transactionTimeout': timeout.inMilliseconds - }); + final Map result = await platform.runTransaction(app.name, transactionId: transactionId, transactionTimeout: timeout.inMilliseconds,); return result ?? {}; } @deprecated Future enablePersistence(bool enable) async { assert(enable != null); - await channel - .invokeMethod('Firestore#enablePersistence', { - 'app': app.name, - 'enable': enable, - }); + await platform.enablePersistence(app.name, enable: enable,); } Future settings( @@ -150,12 +115,8 @@ class Firestore { String host, bool sslEnabled, int cacheSizeBytes}) async { - await channel.invokeMethod('Firestore#settings', { - 'app': app.name, - 'persistenceEnabled': persistenceEnabled, - 'host': host, - 'sslEnabled': sslEnabled, - 'cacheSizeBytes': cacheSizeBytes, - }); + await platform.settings(app.name, persistenceEnabled: persistenceEnabled, + host: host, sslEnabled: sslEnabled, + cacheSizeBytes: cacheSizeBytes); } } diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/query.dart b/packages/cloud_firestore/cloud_firestore/lib/src/query.dart index 2da85859ee5a..62111b5e32a0 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/query.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/query.dart @@ -58,26 +58,15 @@ class Query { StreamController controller; // ignore: close_sinks controller = StreamController.broadcast( onListen: () { - _handle = Firestore.channel.invokeMethod( - 'Query#addSnapshotListener', - { - 'app': firestore.app.name, - 'path': _path, - 'isCollectionGroup': _isCollectionGroup, - 'parameters': _parameters, - 'includeMetadataChanges': includeMetadataChanges, - }, - ).then((dynamic result) => result); + _handle = Firestore.platform.addQuerySnapshotListener(firestore.app.name, path: _path, isCollectionGroup: _isCollectionGroup, parameters: _parameters, includeMetadataChanges: includeMetadataChanges,) + .then((dynamic result) => result); _handle.then((int handle) { Firestore._queryObservers[handle] = controller; }); }, onCancel: () { _handle.then((int handle) async { - await Firestore.channel.invokeMethod( - 'removeListener', - {'handle': handle}, - ); + await Firestore.platform.removeListener(handle); Firestore._queryObservers.remove(handle); }); }, @@ -90,16 +79,7 @@ class Query { {Source source = Source.serverAndCache}) async { assert(source != null); final Map data = - await Firestore.channel.invokeMapMethod( - 'Query#getDocuments', - { - 'app': firestore.app.name, - 'path': _path, - 'isCollectionGroup': _isCollectionGroup, - 'parameters': _parameters, - 'source': _getSourceString(source), - }, - ); + await Firestore.platform.getQueryDocuments(firestore.app.name, path: _path, isCollectionGroup: _isCollectionGroup, parameters: _parameters, source: _getSourceString(source),); return QuerySnapshot._(data, firestore); } diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/transaction.dart b/packages/cloud_firestore/cloud_firestore/lib/src/transaction.dart index d13e3f19f9b2..d653917a7e34 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/transaction.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/transaction.dart @@ -23,12 +23,8 @@ class Transaction { } Future _get(DocumentReference documentReference) async { - final Map result = await Firestore.channel - .invokeMapMethod('Transaction#get', { - 'app': _firestore.app.name, - 'transactionId': _transactionId, - 'path': documentReference.path, - }); + final Map result = await Firestore.platform.getTransaction(_firestore.app.name, transactionId: _transactionId, path: documentReference.path,); + if (result != null) { return DocumentSnapshot._( documentReference.path, @@ -52,12 +48,7 @@ class Transaction { } Future _delete(DocumentReference documentReference) async { - return Firestore.channel - .invokeMethod('Transaction#delete', { - 'app': _firestore.app.name, - 'transactionId': _transactionId, - 'path': documentReference.path, - }); + return Firestore.platform.deleteTransaction(_firestore.app.name, transactionId: _transactionId, path: documentReference.path,); } /// Updates fields in the document referred to by [documentReference]. @@ -74,13 +65,7 @@ class Transaction { Future _update( DocumentReference documentReference, Map data) async { - return Firestore.channel - .invokeMethod('Transaction#update', { - 'app': _firestore.app.name, - 'transactionId': _transactionId, - 'path': documentReference.path, - 'data': data, - }); + return Firestore.platform.updateTransaction(_firestore.app.name, transactionId: _transactionId, path: documentReference.path, data: data); } /// Writes to the document referred to by the provided [DocumentReference]. @@ -98,12 +83,6 @@ class Transaction { Future _set( DocumentReference documentReference, Map data) async { - return Firestore.channel - .invokeMethod('Transaction#set', { - 'app': _firestore.app.name, - 'transactionId': _transactionId, - 'path': documentReference.path, - 'data': data, - }); + return Firestore.platform.setTransaction(_firestore.app.name, transactionId: _transactionId, path: documentReference.path, data: data,); } } diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/write_batch.dart b/packages/cloud_firestore/cloud_firestore/lib/src/write_batch.dart index 6a41ee7bfec3..6142b31f3c15 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/write_batch.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/write_batch.dart @@ -12,8 +12,7 @@ part of cloud_firestore; /// nor can it be committed again. class WriteBatch { WriteBatch._(this._firestore) - : _handle = Firestore.channel.invokeMethod( - 'WriteBatch#create', {'app': _firestore.app.name}); + : _handle = Firestore.platform.createWriteBatch(_firestore.app.name); final Firestore _firestore; Future _handle; @@ -29,8 +28,7 @@ class WriteBatch { if (!_committed) { _committed = true; await Future.wait(_actions); - await Firestore.channel.invokeMethod( - 'WriteBatch#commit', {'handle': await _handle}); + await Firestore.platform.commitWriteBatch(handle: await _handle); } else { throw StateError("This batch has already been committed."); } @@ -41,14 +39,7 @@ class WriteBatch { if (!_committed) { _handle.then((dynamic handle) { _actions.add( - Firestore.channel.invokeMethod( - 'WriteBatch#delete', - { - 'app': _firestore.app.name, - 'handle': handle, - 'path': document.path, - }, - ), + Firestore.platform.deleteWriteBatch(_firestore.app.name, handle: handle, path: document.path,), ); }); } else { @@ -68,16 +59,7 @@ class WriteBatch { if (!_committed) { _handle.then((dynamic handle) { _actions.add( - Firestore.channel.invokeMethod( - 'WriteBatch#setData', - { - 'app': _firestore.app.name, - 'handle': handle, - 'path': document.path, - 'data': data, - 'options': {'merge': merge}, - }, - ), + Firestore.platform.setWriteBatchData(_firestore.app.name, handle: handle, path: document.path, data: data, options: {'merge': merge},), ); }); } else { @@ -93,15 +75,7 @@ class WriteBatch { if (!_committed) { _handle.then((dynamic handle) { _actions.add( - Firestore.channel.invokeMethod( - 'WriteBatch#updateData', - { - 'app': _firestore.app.name, - 'handle': handle, - 'path': document.path, - 'data': data, - }, - ), + Firestore.platform.updateWriteBatchData(_firestore.app.name, handle: handle, path: document.path, data: data,), ); }); } else { diff --git a/packages/cloud_firestore/cloud_firestore/pubspec.yaml b/packages/cloud_firestore/cloud_firestore/pubspec.yaml index 6c756675bdc9..1383cf71a1e9 100755 --- a/packages/cloud_firestore/cloud_firestore/pubspec.yaml +++ b/packages/cloud_firestore/cloud_firestore/pubspec.yaml @@ -13,6 +13,8 @@ flutter: pluginClass: CloudFirestorePlugin dependencies: + cloud_firestore_platform_interface: + path: ../cloud_firestore_platform_interface flutter: sdk: flutter meta: "^1.0.5" diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart index 772749c91e87..387193c04d35 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart @@ -88,7 +88,6 @@ abstract class CloudFirestorePlatform { bool persistenceEnabled, String host, bool sslEnabled, - bool timestampsInSnapshotsEnabled, int cacheSizeBytes, }) async { throw UnimplementedError('settings() is not implemented'); diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart index 14043b45b2b6..ae138393f010 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart @@ -107,7 +107,6 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { bool persistenceEnabled, String host, bool sslEnabled, - bool timestampsInSnapshotsEnabled, int cacheSizeBytes, }) => channel.invokeMethod('Firestore#settings', { @@ -115,7 +114,6 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { 'persistenceEnabled': persistenceEnabled, 'host': host, 'sslEnabled': sslEnabled, - 'timestampsInSnapshotsEnabled': timestampsInSnapshotsEnabled, 'cacheSizeBytes': cacheSizeBytes, }); From 4487365cc50638c0fc4aebecfa25ea07a6d7d072 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 5 Dec 2019 13:37:55 -0800 Subject: [PATCH 04/16] [cloud_firestore_platform_interface] Add transaction delegation. --- .../cloud_firestore_platform_interface.dart | 19 +--- .../src/method_channel_cloud_firestore.dart | 96 +++++++------------ .../lib/src/types.dart | 1 + 3 files changed, 43 insertions(+), 73 deletions(-) create mode 100644 packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types.dart diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart index 387193c04d35..a62dad37cc3c 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart @@ -8,6 +8,10 @@ import 'package:meta/meta.dart' show required, visibleForTesting; import 'src/method_channel_cloud_firestore.dart'; +import 'src/types.dart'; + +export 'src/types.dart'; + /// The interface that implementations of `cloud_firestore` must extend. /// /// Platform implementations should extend this class rather than implement it @@ -59,19 +63,6 @@ abstract class CloudFirestorePlatform { void _verifyProvidesDefaultImplementations() {} // Actual API - // Platform calls - Future onQuerySnapshot(PlatformQuerySnapshot snapshot) async { - throw UnimplementedError('onQuerySnapshot() is not implemented'); - } - - Future onDocumentSnapshot(PlatformDocumentSnapshot snapshot) async { - throw UnimplementedError('onDocumentSnapshot() is not implemented'); - } - - Future onDoTransaction(PlatformTransaction transaction) async { - throw UnimplementedError('onDoTransaction() is not implemented'); - } - // Global /// Removes any listener by its handle. /// All handles must be unique across al types of listeners. @@ -94,7 +85,7 @@ abstract class CloudFirestorePlatform { } Future> runTransaction(String app, { - @required int transactionId, + @required PlatformTransactionHandler transactionHandler, int transactionTimeout, }) async { throw UnimplementedError('runTransaction() is not implemented'); diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart index ae138393f010..7af03bcc67e6 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart @@ -11,28 +11,7 @@ import '../cloud_firestore_platform_interface.dart'; class MethodChannelCloudFirestore extends CloudFirestorePlatform { MethodChannelCloudFirestore() { - channel.setMethodCallHandler((MethodCall call) async { - if (call.method == 'QuerySnapshot') { - final QuerySnapshot snapshot = QuerySnapshot._(call.arguments, this); - _queryObservers[call.arguments['handle']].add(snapshot); - } else if (call.method == 'DocumentSnapshot') { - final DocumentSnapshot snapshot = DocumentSnapshot._( - call.arguments['path'], - _asStringKeyedMap(call.arguments['data']), - SnapshotMetadata._(call.arguments['metadata']['hasPendingWrites'], - call.arguments['metadata']['isFromCache']), - this, - ); - _documentObservers[call.arguments['handle']].add(snapshot); - } else if (call.method == 'DoTransaction') { - final int transactionId = call.arguments['transactionId']; - final Transaction transaction = Transaction(transactionId, this); - final dynamic result = - await _transactionHandlers[transactionId](transaction); - await transaction._finish(); - return result; - } - }); + channel.setMethodCallHandler(_handlePlatformCall); } @visibleForTesting @@ -43,46 +22,26 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { // Platform calls Future _handlePlatformCall(MethodCall call) async { switch(call.method) { - case 'QuerySnapshot': - final QuerySnapshot snapshot = QuerySnapshot._(call.arguments, this); - _queryObservers[call.arguments['handle']].add(snapshot); - break; - case 'DocumentSnapshot': - final DocumentSnapshot snapshot = DocumentSnapshot._( - call.arguments['path'], - _asStringKeyedMap(call.arguments['data']), - SnapshotMetadata._(call.arguments['metadata']['hasPendingWrites'], - call.arguments['metadata']['isFromCache']), - this, - ); - _documentObservers[call.arguments['handle']].add(snapshot); - break; + // case 'QuerySnapshot': + // final QuerySnapshot snapshot = QuerySnapshot._(call.arguments, this); + // _queryObservers[call.arguments['handle']].add(snapshot); + // break; + // case 'DocumentSnapshot': + // final DocumentSnapshot snapshot = DocumentSnapshot._( + // call.arguments['path'], + // _asStringKeyedMap(call.arguments['data']), + // SnapshotMetadata._(call.arguments['metadata']['hasPendingWrites'], + // call.arguments['metadata']['isFromCache']), + // this, + // ); + // _documentObservers[call.arguments['handle']].add(snapshot); + // break; case 'DoTransaction': - final int transactionId = call.arguments['transactionId']; - final Transaction transaction = Transaction(transactionId, this); - final dynamic result = - await _transactionHandlers[transactionId](transaction); - await transaction._finish(); - return result; + return _handleDoTransaction(call); break; } } - @override - Future onQuerySnapshot(PlatformQuerySnapshot snapshot) { - - } - - @override - Future onDocumentSnapshot(PlatformDocumentSnapshot snapshot) { - - } - - @override - Future onDoTransaction(PlatformTransaction transaction) { - - } - // Global @override Future removeListener(int handle) => @@ -117,18 +76,37 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { 'cacheSizeBytes': cacheSizeBytes, }); + // Transaction data + static final Map _transactionHandlers = + {}; + static int _transactionHandlerId = 0; @override Future> runTransaction(String app, { - @required int transactionId, + @required PlatformTransactionHandler transactionHandler, int transactionTimeout, - }) => channel + }) async { + // Store the transaction handler function so we can use it later on DoTransaction... + final int transactionId = _transactionHandlerId++; + _transactionHandlers[transactionId] = transactionHandler; + + return channel .invokeMapMethod( 'Firestore#runTransaction', { 'app': app, 'transactionId': transactionId, 'transactionTimeout': transactionTimeout }); + } + + Future _handleDoTransaction(MethodCall call) async { + final int transactionId = call.arguments['transactionId']; + final PlatformTransactionHandler transactionHandler = _transactionHandlers[transactionId]; + // Delete the handler from the list... + _transactionHandlers.remove(transactionId); + // Delegate handling to the implementation + return await transactionHandler(transactionId); + } // Document Reference @override diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types.dart new file mode 100644 index 000000000000..f7b3dfeb3b51 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types.dart @@ -0,0 +1 @@ +typedef Future PlatformTransactionHandler(int transactionId); From 0f505c5fd9e7d9b1289b3f9bc1ce829b25ed2cea Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 5 Dec 2019 13:38:40 -0800 Subject: [PATCH 05/16] [cloud_firestore] Migrate runTransaction to platform interface. --- .../cloud_firestore/lib/src/firestore.dart | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart index 32e236383a0f..b31fb1cee754 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart @@ -32,10 +32,6 @@ class Firestore { static final Map> _documentObservers = >{}; - static final Map _transactionHandlers = - {}; - static int _transactionHandlerId = 0; - @override bool operator ==(dynamic o) => o is Firestore && o.app == app; @@ -98,9 +94,17 @@ class Firestore { {Duration timeout = const Duration(seconds: 5)}) async { assert(timeout.inMilliseconds > 0, 'Transaction timeout must be more than 0 milliseconds'); - final int transactionId = _transactionHandlerId++; - _transactionHandlers[transactionId] = transactionHandler; - final Map result = await platform.runTransaction(app.name, transactionId: transactionId, transactionTimeout: timeout.inMilliseconds,); + + // Wrap the transactionHandler into something that can be passed to the Platform implementation + final PlatformTransactionHandler handler = (int transactionId) async { + Transaction transaction = Transaction(transactionId, this); + final dynamic result = await transactionHandler(transaction); + await transaction._finish(); + return result; + }; + + // Move to a runTransaction method in the method channel! + final Map result = await platform.runTransaction(app.name, transactionHandler: handler, transactionTimeout: timeout.inMilliseconds,); return result ?? {}; } From fe727758de9b3efa257c5de565bf29b650495c79 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 5 Dec 2019 17:31:27 -0800 Subject: [PATCH 06/16] [cloud_firestore_platform_interface] Move query snapshots to a stream. --- .../cloud_firestore_platform_interface.dart | 7 ++- .../src/method_channel_cloud_firestore.dart | 51 ++++++++++++++++--- .../lib/src/types.dart | 2 + 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart index a62dad37cc3c..de1f0622aaed 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart @@ -131,14 +131,13 @@ abstract class CloudFirestorePlatform { } // Query - // TODO: Port to stream - Future addQuerySnapshotListener(String app, { + Stream snapshots(String app, { @required String path, bool isCollectionGroup, Map parameters, bool includeMetadataChanges, - }) async { - throw UnimplementedError('addQuerySnapshotListener() is not implemented'); + }) { + throw UnimplementedError('snapshots() is not implemented'); } //TODO: Type this return diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart index 7af03bcc67e6..ab39d9d02f46 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart @@ -22,10 +22,9 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { // Platform calls Future _handlePlatformCall(MethodCall call) async { switch(call.method) { - // case 'QuerySnapshot': - // final QuerySnapshot snapshot = QuerySnapshot._(call.arguments, this); - // _queryObservers[call.arguments['handle']].add(snapshot); - // break; + case 'QuerySnapshot': + return _handleQuerySnapshot(call); + break; // case 'DocumentSnapshot': // final DocumentSnapshot snapshot = DocumentSnapshot._( // call.arguments['path'], @@ -175,9 +174,7 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { ); // Query - // TODO: Port to stream - @override - Future addQuerySnapshotListener(String app, { + Future _addQuerySnapshotListener(String app, { @required String path, bool isCollectionGroup, Map parameters, @@ -193,6 +190,46 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { }, ); + static final Map> _queryObservers = + >{}; + + @override + Stream snapshots(String app, { + @required String path, + bool isCollectionGroup, + Map parameters, + bool includeMetadataChanges, + }) { + // Create a stream of query snapshot handles, as they happen + 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. + StreamController controller; // ignore: close_sinks + controller = StreamController.broadcast( + onListen: () { + _handle = _addQuerySnapshotListener(app, path: path, isCollectionGroup: isCollectionGroup, parameters: parameters, includeMetadataChanges: includeMetadataChanges,) + .then((dynamic result) => result); + _handle.then((int handle) { + _queryObservers[handle] = controller; + }); + }, + onCancel: () { + _handle.then((int handle) async { + await removeListener(handle); + _queryObservers.remove(handle); + }); + }, + ); + return controller.stream; + } + + // Broadcast the QuerySnapshot data + void _handleQuerySnapshot(MethodCall call) { + final int handle = call.arguments['handle']; + _queryObservers[handle].add(call.arguments); + } + //TODO: Type this return @override Future> getQueryDocuments(String app, { diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types.dart index f7b3dfeb3b51..2746b6008f64 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/types.dart @@ -1 +1,3 @@ typedef Future PlatformTransactionHandler(int transactionId); + +// Data required to initialize a snapshot From 3f124b92bd910a4a77353e969d939875a32bc25b Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 5 Dec 2019 17:31:50 -0800 Subject: [PATCH 07/16] [cloud_firestore] Use the stream from the plugin. --- .../cloud_firestore/lib/cloud_firestore.dart | 1 + .../cloud_firestore/lib/src/firestore.dart | 3 --- .../cloud_firestore/lib/src/query.dart | 23 +++---------------- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart index fce55bfdc3fe..ae680dd994f0 100755 --- a/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart @@ -10,6 +10,7 @@ import 'dart:typed_data'; import 'dart:ui' show hashValues, hashList; import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; + import 'package:collection/collection.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart index b31fb1cee754..a9d400ff250d 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart @@ -26,9 +26,6 @@ class Firestore { @visibleForTesting static CloudFirestorePlatform platform = CloudFirestorePlatform.instance; - static final Map> _queryObservers = - >{}; - static final Map> _documentObservers = >{}; diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/query.dart b/packages/cloud_firestore/cloud_firestore/lib/src/query.dart index 62111b5e32a0..e316b76da351 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/query.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/query.dart @@ -52,26 +52,9 @@ class Query { // TODO(jackson): Reduce code duplication with [DocumentReference] 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. - StreamController controller; // ignore: close_sinks - controller = StreamController.broadcast( - onListen: () { - _handle = Firestore.platform.addQuerySnapshotListener(firestore.app.name, path: _path, isCollectionGroup: _isCollectionGroup, parameters: _parameters, includeMetadataChanges: includeMetadataChanges,) - .then((dynamic result) => result); - _handle.then((int handle) { - Firestore._queryObservers[handle] = controller; - }); - }, - onCancel: () { - _handle.then((int handle) async { - await Firestore.platform.removeListener(handle); - Firestore._queryObservers.remove(handle); - }); - }, - ); - return controller.stream; + + return Firestore.platform.snapshots(firestore.app.name, path: _path, isCollectionGroup: _isCollectionGroup, parameters: _parameters, includeMetadataChanges: includeMetadataChanges,) + .map((dynamic data) => QuerySnapshot._(data, firestore)); } /// Fetch the documents for this query From 85a40731c3a9e269f8e92bd0a2ac93f974d41382 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 5 Dec 2019 18:16:34 -0800 Subject: [PATCH 08/16] [cloud_firestore_platform_interface] Rename 'snapshots' to 'getQuerySnapshots' This is to prevent naming collisions with the same method from Document References. --- .../lib/cloud_firestore_platform_interface.dart | 4 ++-- .../lib/src/method_channel_cloud_firestore.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart index de1f0622aaed..53ddc360e24b 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart @@ -131,13 +131,13 @@ abstract class CloudFirestorePlatform { } // Query - Stream snapshots(String app, { + Stream getQuerySnapshots(String app, { @required String path, bool isCollectionGroup, Map parameters, bool includeMetadataChanges, }) { - throw UnimplementedError('snapshots() is not implemented'); + throw UnimplementedError('getQuerySnapshots() is not implemented'); } //TODO: Type this return diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart index ab39d9d02f46..1f479057e82a 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart @@ -194,7 +194,7 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { >{}; @override - Stream snapshots(String app, { + Stream getQuerySnapshots(String app, { @required String path, bool isCollectionGroup, Map parameters, From 0cfd3d79f9588609836227c2b48c036c86535f2d Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 5 Dec 2019 18:17:46 -0800 Subject: [PATCH 09/16] [cloud_firestore] Use renamed getQuerySnapshots --- packages/cloud_firestore/cloud_firestore/lib/src/query.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/query.dart b/packages/cloud_firestore/cloud_firestore/lib/src/query.dart index e316b76da351..19f43a759715 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/query.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/query.dart @@ -53,7 +53,7 @@ class Query { Stream snapshots({bool includeMetadataChanges = false}) { assert(includeMetadataChanges != null); - return Firestore.platform.snapshots(firestore.app.name, path: _path, isCollectionGroup: _isCollectionGroup, parameters: _parameters, includeMetadataChanges: includeMetadataChanges,) + return Firestore.platform.getQuerySnapshots(firestore.app.name, path: _path, isCollectionGroup: _isCollectionGroup, parameters: _parameters, includeMetadataChanges: includeMetadataChanges,) .map((dynamic data) => QuerySnapshot._(data, firestore)); } From 32fa7ee5f5f60680f6f65d0af0a686bb054ef1dc Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 5 Dec 2019 19:03:58 -0800 Subject: [PATCH 10/16] [cloud_firestore_platform_interface] Create DocumentReferenceSnapshots stream. (Corrected return type of Stream controller for the Query snapshots as well) --- .../cloud_firestore_platform_interface.dart | 5 +- .../src/method_channel_cloud_firestore.dart | 57 ++++++++++++++----- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart index 53ddc360e24b..ef5e1584b367 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart @@ -122,11 +122,10 @@ abstract class CloudFirestorePlatform { throw UnimplementedError('deleteDocumentReference() is not implemented'); } - // TODO: Port to stream - Future addDocumentReferenceSnapshotListener(String app, { + Stream getDocumentReferenceSnapshots(String app, { @required String path, bool includeMetadataChanges, - }) async { + }) { throw UnimplementedError('addDocumentReferenceSnapshotListener() is not implemented'); } diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart index 1f479057e82a..d9a68df0cce8 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart @@ -25,16 +25,9 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { case 'QuerySnapshot': return _handleQuerySnapshot(call); break; - // case 'DocumentSnapshot': - // final DocumentSnapshot snapshot = DocumentSnapshot._( - // call.arguments['path'], - // _asStringKeyedMap(call.arguments['data']), - // SnapshotMetadata._(call.arguments['metadata']['hasPendingWrites'], - // call.arguments['metadata']['isFromCache']), - // this, - // ); - // _documentObservers[call.arguments['handle']].add(snapshot); - // break; + case 'DocumentSnapshot': + return _handleDocumentSnapshot(call); + break; case 'DoTransaction': return _handleDoTransaction(call); break; @@ -159,9 +152,7 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { {'app': app, 'path': path}, ); - // TODO: Port to stream - @override - Future addDocumentReferenceSnapshotListener(String app, { + Future _addDocumentReferenceSnapshotListener(String app, { @required String path, bool includeMetadataChanges, }) => channel.invokeMethod( @@ -173,6 +164,42 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { }, ); + static final Map> _documentObservers = + >{}; + + // This method is very similar to getQuerySnapshots. Extract common logic? + @override + Stream getDocumentReferenceSnapshots(String app, { + @required String path, + bool includeMetadataChanges, + }) { + 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. + StreamController controller; // ignore: close_sinks + controller = StreamController.broadcast( + onListen: () { + _handle = _addDocumentReferenceSnapshotListener(app, path: path, includeMetadataChanges: includeMetadataChanges,) + .then((dynamic result) => result); + _handle.then((int handle) { + _documentObservers[handle] = controller; + }); + }, + onCancel: () { + _handle.then((int handle) async { + await removeListener(handle); + _documentObservers.remove(handle); + }); + }, + ); + return controller.stream; + } + + void _handleDocumentSnapshot(MethodCall call) { + final int handle = call.arguments['handle']; + _queryObservers[handle].add(call.arguments); + } + // Query Future _addQuerySnapshotListener(String app, { @required String path, @@ -205,8 +232,8 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { 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. - StreamController controller; // ignore: close_sinks - controller = StreamController.broadcast( + StreamController controller; // ignore: close_sinks + controller = StreamController.broadcast( onListen: () { _handle = _addQuerySnapshotListener(app, path: path, isCollectionGroup: isCollectionGroup, parameters: parameters, includeMetadataChanges: includeMetadataChanges,) .then((dynamic result) => result); From d0663ceccdac592e8617bd40ffed260b1dea23ab Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 5 Dec 2019 19:04:58 -0800 Subject: [PATCH 11/16] [cloud_firestore] Use the new stream for document_references. --- .../lib/src/document_reference.dart | 29 ++++++------------- .../cloud_firestore/lib/src/firestore.dart | 3 -- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart b/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart index e8b1ca676fd5..951e15a2c125 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart @@ -94,25 +94,14 @@ class DocumentReference { // TODO(jackson): Reduce code duplication with [Query] 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. - StreamController controller; // ignore: close_sinks - controller = StreamController.broadcast( - onListen: () { - _handle = Firestore.platform.addDocumentReferenceSnapshotListener(firestore.app.name, path: path, includeMetadataChanges: includeMetadataChanges,) - .then((dynamic result) => result); - _handle.then((int handle) { - Firestore._documentObservers[handle] = controller; - }); - }, - onCancel: () { - _handle.then((int handle) async { - await Firestore.platform.removeListener(handle); - Firestore._documentObservers.remove(handle); - }); - }, - ); - return controller.stream; + + return Firestore.platform.getDocumentReferenceSnapshots(firestore.app.name, path: path, includeMetadataChanges: includeMetadataChanges,) + .map((dynamic data) => DocumentSnapshot._( + data['path'], + _asStringKeyedMap(data['data']), + SnapshotMetadata._(data['metadata']['hasPendingWrites'], + data['metadata']['isFromCache']), + firestore, + )); } } diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart index a9d400ff250d..364128b88a3b 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart @@ -26,9 +26,6 @@ class Firestore { @visibleForTesting static CloudFirestorePlatform platform = CloudFirestorePlatform.instance; - static final Map> _documentObservers = - >{}; - @override bool operator ==(dynamic o) => o is Firestore && o.app == app; From 6fd3a006d525fc053ac41a1d49c34bf6c18148a4 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 5 Dec 2019 19:06:02 -0800 Subject: [PATCH 12/16] [cloud_firestore] dartfmt --- .../lib/src/document_reference.dart | 45 ++++++++++++++----- .../cloud_firestore/lib/src/firestore.dart | 17 +++++-- .../cloud_firestore/lib/src/query.dart | 19 ++++++-- .../cloud_firestore/lib/src/transaction.dart | 24 ++++++++-- .../cloud_firestore/lib/src/write_batch.dart | 21 +++++++-- 5 files changed, 100 insertions(+), 26 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart b/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart index 951e15a2c125..ad66ca77e956 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/document_reference.dart @@ -48,7 +48,12 @@ class DocumentReference { /// If [merge] is true, the provided data will be merged into an /// existing document instead of overwriting. Future setData(Map data, {bool merge = false}) { - return Firestore.platform.setDocumentReferenceData(firestore.app.name, path: path, data: data, options: {'merge': merge},); + return Firestore.platform.setDocumentReferenceData( + firestore.app.name, + path: path, + data: data, + options: {'merge': merge}, + ); } /// Updates fields in the document referred to by this [DocumentReference]. @@ -58,7 +63,11 @@ class DocumentReference { /// /// If no document exists yet, the update will fail. Future updateData(Map data) { - return Firestore.platform.updateDocumentReferenceData(firestore.app.name, path: path, data: data,); + return Firestore.platform.updateDocumentReferenceData( + firestore.app.name, + path: path, + data: data, + ); } /// Reads the document referenced by this [DocumentReference]. @@ -66,7 +75,11 @@ class DocumentReference { /// If no document exists, the read will return null. Future get({Source source = Source.serverAndCache}) async { final Map data = - await Firestore.platform.getDocumentReference(firestore.app.name, path: path, source: _getSourceString(source),); + await Firestore.platform.getDocumentReference( + firestore.app.name, + path: path, + source: _getSourceString(source), + ); return DocumentSnapshot._( data['path'], @@ -79,7 +92,10 @@ class DocumentReference { /// Deletes the document referred to by this [DocumentReference]. Future delete() { - return Firestore.platform.deleteDocumentReference(firestore.app.name, path: path,); + return Firestore.platform.deleteDocumentReference( + firestore.app.name, + path: path, + ); } /// Returns the reference of a collection contained inside of this @@ -95,13 +111,18 @@ class DocumentReference { Stream snapshots({bool includeMetadataChanges = false}) { assert(includeMetadataChanges != null); - return Firestore.platform.getDocumentReferenceSnapshots(firestore.app.name, path: path, includeMetadataChanges: includeMetadataChanges,) - .map((dynamic data) => DocumentSnapshot._( - data['path'], - _asStringKeyedMap(data['data']), - SnapshotMetadata._(data['metadata']['hasPendingWrites'], - data['metadata']['isFromCache']), - firestore, - )); + return Firestore.platform + .getDocumentReferenceSnapshots( + firestore.app.name, + path: path, + includeMetadataChanges: includeMetadataChanges, + ) + .map((dynamic data) => DocumentSnapshot._( + data['path'], + _asStringKeyedMap(data['data']), + SnapshotMetadata._(data['metadata']['hasPendingWrites'], + data['metadata']['isFromCache']), + firestore, + )); } } diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart index 364128b88a3b..0c69359bc615 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart @@ -98,14 +98,21 @@ class Firestore { }; // Move to a runTransaction method in the method channel! - final Map result = await platform.runTransaction(app.name, transactionHandler: handler, transactionTimeout: timeout.inMilliseconds,); + final Map result = await platform.runTransaction( + app.name, + transactionHandler: handler, + transactionTimeout: timeout.inMilliseconds, + ); return result ?? {}; } @deprecated Future enablePersistence(bool enable) async { assert(enable != null); - await platform.enablePersistence(app.name, enable: enable,); + await platform.enablePersistence( + app.name, + enable: enable, + ); } Future settings( @@ -113,8 +120,10 @@ class Firestore { String host, bool sslEnabled, int cacheSizeBytes}) async { - await platform.settings(app.name, persistenceEnabled: persistenceEnabled, - host: host, sslEnabled: sslEnabled, + await platform.settings(app.name, + persistenceEnabled: persistenceEnabled, + host: host, + sslEnabled: sslEnabled, cacheSizeBytes: cacheSizeBytes); } } diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/query.dart b/packages/cloud_firestore/cloud_firestore/lib/src/query.dart index 19f43a759715..170835ad07ec 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/query.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/query.dart @@ -53,8 +53,15 @@ class Query { Stream snapshots({bool includeMetadataChanges = false}) { assert(includeMetadataChanges != null); - return Firestore.platform.getQuerySnapshots(firestore.app.name, path: _path, isCollectionGroup: _isCollectionGroup, parameters: _parameters, includeMetadataChanges: includeMetadataChanges,) - .map((dynamic data) => QuerySnapshot._(data, firestore)); + return Firestore.platform + .getQuerySnapshots( + firestore.app.name, + path: _path, + isCollectionGroup: _isCollectionGroup, + parameters: _parameters, + includeMetadataChanges: includeMetadataChanges, + ) + .map((dynamic data) => QuerySnapshot._(data, firestore)); } /// Fetch the documents for this query @@ -62,7 +69,13 @@ class Query { {Source source = Source.serverAndCache}) async { assert(source != null); final Map data = - await Firestore.platform.getQueryDocuments(firestore.app.name, path: _path, isCollectionGroup: _isCollectionGroup, parameters: _parameters, source: _getSourceString(source),); + await Firestore.platform.getQueryDocuments( + firestore.app.name, + path: _path, + isCollectionGroup: _isCollectionGroup, + parameters: _parameters, + source: _getSourceString(source), + ); return QuerySnapshot._(data, firestore); } diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/transaction.dart b/packages/cloud_firestore/cloud_firestore/lib/src/transaction.dart index d653917a7e34..c65c8ba77e04 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/transaction.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/transaction.dart @@ -23,7 +23,11 @@ class Transaction { } Future _get(DocumentReference documentReference) async { - final Map result = await Firestore.platform.getTransaction(_firestore.app.name, transactionId: _transactionId, path: documentReference.path,); + final Map result = await Firestore.platform.getTransaction( + _firestore.app.name, + transactionId: _transactionId, + path: documentReference.path, + ); if (result != null) { return DocumentSnapshot._( @@ -48,7 +52,11 @@ class Transaction { } Future _delete(DocumentReference documentReference) async { - return Firestore.platform.deleteTransaction(_firestore.app.name, transactionId: _transactionId, path: documentReference.path,); + return Firestore.platform.deleteTransaction( + _firestore.app.name, + transactionId: _transactionId, + path: documentReference.path, + ); } /// Updates fields in the document referred to by [documentReference]. @@ -65,7 +73,10 @@ class Transaction { Future _update( DocumentReference documentReference, Map data) async { - return Firestore.platform.updateTransaction(_firestore.app.name, transactionId: _transactionId, path: documentReference.path, data: data); + return Firestore.platform.updateTransaction(_firestore.app.name, + transactionId: _transactionId, + path: documentReference.path, + data: data); } /// Writes to the document referred to by the provided [DocumentReference]. @@ -83,6 +94,11 @@ class Transaction { Future _set( DocumentReference documentReference, Map data) async { - return Firestore.platform.setTransaction(_firestore.app.name, transactionId: _transactionId, path: documentReference.path, data: data,); + return Firestore.platform.setTransaction( + _firestore.app.name, + transactionId: _transactionId, + path: documentReference.path, + data: data, + ); } } diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/write_batch.dart b/packages/cloud_firestore/cloud_firestore/lib/src/write_batch.dart index 6142b31f3c15..e3ced8aff2f5 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/write_batch.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/write_batch.dart @@ -39,7 +39,11 @@ class WriteBatch { if (!_committed) { _handle.then((dynamic handle) { _actions.add( - Firestore.platform.deleteWriteBatch(_firestore.app.name, handle: handle, path: document.path,), + Firestore.platform.deleteWriteBatch( + _firestore.app.name, + handle: handle, + path: document.path, + ), ); }); } else { @@ -59,7 +63,13 @@ class WriteBatch { if (!_committed) { _handle.then((dynamic handle) { _actions.add( - Firestore.platform.setWriteBatchData(_firestore.app.name, handle: handle, path: document.path, data: data, options: {'merge': merge},), + Firestore.platform.setWriteBatchData( + _firestore.app.name, + handle: handle, + path: document.path, + data: data, + options: {'merge': merge}, + ), ); }); } else { @@ -75,7 +85,12 @@ class WriteBatch { if (!_committed) { _handle.then((dynamic handle) { _actions.add( - Firestore.platform.updateWriteBatchData(_firestore.app.name, handle: handle, path: document.path, data: data,), + Firestore.platform.updateWriteBatchData( + _firestore.app.name, + handle: handle, + path: document.path, + data: data, + ), ); }); } else { From 6610d18adff6c65f285108deed102bfb76cac820 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 5 Dec 2019 19:06:24 -0800 Subject: [PATCH 13/16] [cloud_firestore_platform_interface] dartfmt --- .../cloud_firestore_platform_interface.dart | 68 ++-- .../src/method_channel_cloud_firestore.dart | 366 ++++++++++-------- ...oud_firestore_platform_interface_test.dart | 3 +- .../method_channel_firebase_core_test.dart | 3 - 4 files changed, 245 insertions(+), 195 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart index ef5e1584b367..48d6374aca1b 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart @@ -62,7 +62,7 @@ abstract class CloudFirestorePlatform { /// if the provided instance is a class implemented with `implements`. void _verifyProvidesDefaultImplementations() {} - // Actual API + // Actual API // Global /// Removes any listener by its handle. /// All handles must be unique across al types of listeners. @@ -75,16 +75,18 @@ abstract class CloudFirestorePlatform { throw UnimplementedError('enablePersistence() is not implemented'); } - Future settings(String app, { - bool persistenceEnabled, - String host, - bool sslEnabled, - int cacheSizeBytes, - }) async { + Future settings( + String app, { + bool persistenceEnabled, + String host, + bool sslEnabled, + int cacheSizeBytes, + }) async { throw UnimplementedError('settings() is not implemented'); } - Future> runTransaction(String app, { + Future> runTransaction( + String app, { @required PlatformTransactionHandler transactionHandler, int transactionTimeout, }) async { @@ -92,45 +94,53 @@ abstract class CloudFirestorePlatform { } // Document Reference - Future setDocumentReferenceData(String app, { + Future setDocumentReferenceData( + String app, { @required String path, Map data, - // TODO: Type https://firebase.google.com/docs/reference/js/firebase.firestore.SetOptions.html + // TODO: Type https://firebase.google.com/docs/reference/js/firebase.firestore.SetOptions.html Map options, }) async { throw UnimplementedError('setDocumentReferenceData() is not implemented'); } - Future updateDocumentReferenceData(String app, { + Future updateDocumentReferenceData( + String app, { @required String path, Map data, }) async { - throw UnimplementedError('updateDocumentReferenceData() is not implemented'); + throw UnimplementedError( + 'updateDocumentReferenceData() is not implemented'); } // TODO: Type this return - Future> getDocumentReference(String app, { + Future> getDocumentReference( + String app, { @required String path, @required String source, }) async { throw UnimplementedError('getDocumentReference() is not implemented'); } - Future deleteDocumentReference(String app, { + Future deleteDocumentReference( + String app, { @required String path, }) async { throw UnimplementedError('deleteDocumentReference() is not implemented'); } - Stream getDocumentReferenceSnapshots(String app, { + Stream getDocumentReferenceSnapshots( + String app, { @required String path, bool includeMetadataChanges, }) { - throw UnimplementedError('addDocumentReferenceSnapshotListener() is not implemented'); + throw UnimplementedError( + 'addDocumentReferenceSnapshotListener() is not implemented'); } // Query - Stream getQuerySnapshots(String app, { + Stream getQuerySnapshots( + String app, { @required String path, bool isCollectionGroup, Map parameters, @@ -140,7 +150,8 @@ abstract class CloudFirestorePlatform { } //TODO: Type this return - Future> getQueryDocuments(String app, { + Future> getQueryDocuments( + String app, { @required String path, bool isCollectionGroup, Map parameters, @@ -151,21 +162,24 @@ abstract class CloudFirestorePlatform { // Transaction // TODO: Type this return - Future> getTransaction(String app, { + Future> getTransaction( + String app, { @required String path, @required int transactionId, }) async { throw UnimplementedError('getTransaction() is not implemented'); } - Future deleteTransaction(String app, { + Future deleteTransaction( + String app, { @required String path, @required int transactionId, }) async { throw UnimplementedError('deleteTransaction() is not implemented'); } - Future updateTransaction(String app, { + Future updateTransaction( + String app, { @required String path, @required int transactionId, Map data, @@ -173,7 +187,8 @@ abstract class CloudFirestorePlatform { throw UnimplementedError('updateTransaction() is not implemented'); } - Future setTransaction(String app, { + Future setTransaction( + String app, { @required String path, @required int transactionId, Map data, @@ -192,14 +207,16 @@ abstract class CloudFirestorePlatform { throw UnimplementedError('commitWriteBatch() is not implemented'); } - Future deleteWriteBatch(String app, { + Future deleteWriteBatch( + String app, { @required dynamic handle, @required String path, }) async { throw UnimplementedError('deleteWriteBatch() is not implemented'); } - Future setWriteBatchData(String app, { + Future setWriteBatchData( + String app, { @required dynamic handle, @required String path, Map data, @@ -208,7 +225,8 @@ abstract class CloudFirestorePlatform { throw UnimplementedError('setWriteBatchData() is not implemented'); } - Future updateWriteBatchData(String app, { + Future updateWriteBatchData( + String app, { @required dynamic handle, @required String path, Map data, diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart index d9a68df0cce8..37de25474f24 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart @@ -21,7 +21,7 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { // Platform calls Future _handlePlatformCall(MethodCall call) async { - switch(call.method) { + switch (call.method) { case 'QuerySnapshot': return _handleQuerySnapshot(call); break; @@ -36,37 +36,34 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { // Global @override - Future removeListener(int handle) => - channel.invokeMethod( - 'removeListener', - {'handle': handle}, - ); - + Future removeListener(int handle) => channel.invokeMethod( + 'removeListener', + {'handle': handle}, + ); // Firestore @override - Future enablePersistence(String app, {bool enable = true}) => - channel - .invokeMethod('Firestore#enablePersistence', { - 'app': app, - 'enable': enable, - }); - + Future enablePersistence(String app, {bool enable = true}) => channel + .invokeMethod('Firestore#enablePersistence', { + 'app': app, + 'enable': enable, + }); @override - Future settings(String app, { - bool persistenceEnabled, - String host, - bool sslEnabled, - int cacheSizeBytes, - }) => - channel.invokeMethod('Firestore#settings', { - 'app': app, - 'persistenceEnabled': persistenceEnabled, - 'host': host, - 'sslEnabled': sslEnabled, - 'cacheSizeBytes': cacheSizeBytes, - }); + Future settings( + String app, { + bool persistenceEnabled, + String host, + bool sslEnabled, + int cacheSizeBytes, + }) => + channel.invokeMethod('Firestore#settings', { + 'app': app, + 'persistenceEnabled': persistenceEnabled, + 'host': host, + 'sslEnabled': sslEnabled, + 'cacheSizeBytes': cacheSizeBytes, + }); // Transaction data static final Map _transactionHandlers = @@ -74,7 +71,8 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { static int _transactionHandlerId = 0; @override - Future> runTransaction(String app, { + Future> runTransaction( + String app, { @required PlatformTransactionHandler transactionHandler, int transactionTimeout, }) async { @@ -82,9 +80,8 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { final int transactionId = _transactionHandlerId++; _transactionHandlers[transactionId] = transactionHandler; - return channel - .invokeMapMethod( - 'Firestore#runTransaction', { + return channel.invokeMapMethod( + 'Firestore#runTransaction', { 'app': app, 'transactionId': transactionId, 'transactionTimeout': transactionTimeout @@ -93,7 +90,8 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { Future _handleDoTransaction(MethodCall call) async { final int transactionId = call.arguments['transactionId']; - final PlatformTransactionHandler transactionHandler = _transactionHandlers[transactionId]; + final PlatformTransactionHandler transactionHandler = + _transactionHandlers[transactionId]; // Delete the handler from the list... _transactionHandlers.remove(transactionId); // Delegate handling to the implementation @@ -102,74 +100,85 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { // Document Reference @override - Future setDocumentReferenceData(String app, { + Future setDocumentReferenceData( + String app, { @required String path, // TODO: Type SetOptions: https://firebase.google.com/docs/reference/js/firebase.firestore.SetOptions.html Map options, Map data = const {}, - }) => channel.invokeMethod( - 'DocumentReference#setData', - { - 'app': app, - 'path': path, - 'data': data, - 'options': options, - }, - ); + }) => + channel.invokeMethod( + 'DocumentReference#setData', + { + 'app': app, + 'path': path, + 'data': data, + 'options': options, + }, + ); @override - Future updateDocumentReferenceData(String app, { + Future updateDocumentReferenceData( + String app, { @required String path, Map data = const {}, - }) => channel.invokeMethod( - 'DocumentReference#updateData', - { - 'app': app, - 'path': path, - 'data': data, - }, - ); + }) => + channel.invokeMethod( + 'DocumentReference#updateData', + { + 'app': app, + 'path': path, + 'data': data, + }, + ); // TODO: Type this return @override - Future> getDocumentReference(String app, { + Future> getDocumentReference( + String app, { @required String path, @required String source, - }) => channel.invokeMapMethod( - 'DocumentReference#get', - { - 'app': app, - 'path': path, - 'source': source, - }, - ); + }) => + channel.invokeMapMethod( + 'DocumentReference#get', + { + 'app': app, + 'path': path, + 'source': source, + }, + ); @override - Future deleteDocumentReference(String app, { + Future deleteDocumentReference( + String app, { @required String path, - }) => channel.invokeMethod( - 'DocumentReference#delete', - {'app': app, 'path': path}, - ); - - Future _addDocumentReferenceSnapshotListener(String app, { + }) => + channel.invokeMethod( + 'DocumentReference#delete', + {'app': app, 'path': path}, + ); + + Future _addDocumentReferenceSnapshotListener( + String app, { @required String path, bool includeMetadataChanges, - }) => channel.invokeMethod( - 'DocumentReference#addSnapshotListener', - { - 'app': app, - 'path': path, - 'includeMetadataChanges': includeMetadataChanges, - }, - ); + }) => + channel.invokeMethod( + 'DocumentReference#addSnapshotListener', + { + 'app': app, + 'path': path, + 'includeMetadataChanges': includeMetadataChanges, + }, + ); static final Map> _documentObservers = >{}; // This method is very similar to getQuerySnapshots. Extract common logic? @override - Stream getDocumentReferenceSnapshots(String app, { + Stream getDocumentReferenceSnapshots( + String app, { @required String path, bool includeMetadataChanges, }) { @@ -179,8 +188,11 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { StreamController controller; // ignore: close_sinks controller = StreamController.broadcast( onListen: () { - _handle = _addDocumentReferenceSnapshotListener(app, path: path, includeMetadataChanges: includeMetadataChanges,) - .then((dynamic result) => result); + _handle = _addDocumentReferenceSnapshotListener( + app, + path: path, + includeMetadataChanges: includeMetadataChanges, + ).then((dynamic result) => result); _handle.then((int handle) { _documentObservers[handle] = controller; }); @@ -201,42 +213,50 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { } // Query - Future _addQuerySnapshotListener(String app, { + Future _addQuerySnapshotListener( + String app, { @required String path, bool isCollectionGroup, Map parameters, bool includeMetadataChanges, - }) => channel.invokeMethod( - 'Query#addSnapshotListener', - { - 'app': app, - 'path': path, - 'isCollectionGroup': isCollectionGroup, - 'parameters': parameters, - 'includeMetadataChanges': includeMetadataChanges, - }, - ); + }) => + channel.invokeMethod( + 'Query#addSnapshotListener', + { + 'app': app, + 'path': path, + 'isCollectionGroup': isCollectionGroup, + 'parameters': parameters, + 'includeMetadataChanges': includeMetadataChanges, + }, + ); static final Map> _queryObservers = >{}; @override - Stream getQuerySnapshots(String app, { + Stream getQuerySnapshots( + String app, { @required String path, bool isCollectionGroup, Map parameters, bool includeMetadataChanges, }) { // Create a stream of query snapshot handles, as they happen - assert(includeMetadataChanges != null); + 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. StreamController controller; // ignore: close_sinks controller = StreamController.broadcast( onListen: () { - _handle = _addQuerySnapshotListener(app, path: path, isCollectionGroup: isCollectionGroup, parameters: parameters, includeMetadataChanges: includeMetadataChanges,) - .then((dynamic result) => result); + _handle = _addQuerySnapshotListener( + app, + path: path, + isCollectionGroup: isCollectionGroup, + parameters: parameters, + includeMetadataChanges: includeMetadataChanges, + ).then((dynamic result) => result); _handle.then((int handle) { _queryObservers[handle] = controller; }); @@ -259,125 +279,139 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { //TODO: Type this return @override - Future> getQueryDocuments(String app, { + Future> getQueryDocuments( + String app, { @required String path, bool isCollectionGroup, Map parameters, String source, - }) => channel.invokeMapMethod( - 'Query#getDocuments', - { - 'app': app, - 'path': path, - 'isCollectionGroup': isCollectionGroup, - 'parameters': parameters, - 'source': source, - }, - ); + }) => + channel.invokeMapMethod( + 'Query#getDocuments', + { + 'app': app, + 'path': path, + 'isCollectionGroup': isCollectionGroup, + 'parameters': parameters, + 'source': source, + }, + ); // Transaction // TODO: Type this return @override - Future> getTransaction(String app, { + Future> getTransaction( + String app, { @required String path, @required int transactionId, - }) => channel - .invokeMapMethod('Transaction#get', { - 'app': app, - 'transactionId': transactionId, - 'path': path, - }); + }) => + channel.invokeMapMethod( + 'Transaction#get', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + }); @override - Future deleteTransaction(String app, { + Future deleteTransaction( + String app, { @required String path, @required int transactionId, - }) => channel - .invokeMethod('Transaction#delete', { - 'app': app, - 'transactionId': transactionId, - 'path': path, - }); + }) => + channel.invokeMethod('Transaction#delete', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + }); @override - Future updateTransaction(String app, { + Future updateTransaction( + String app, { @required String path, @required int transactionId, Map data, - }) => channel - .invokeMethod('Transaction#update', { - 'app': app, - 'transactionId': transactionId, - 'path': path, - 'data': data, - }); + }) => + channel.invokeMethod('Transaction#update', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + 'data': data, + }); @override - Future setTransaction(String app, { + Future setTransaction( + String app, { @required String path, @required int transactionId, Map data, - }) => channel - .invokeMethod('Transaction#set', { - 'app': app, - 'transactionId': transactionId, - 'path': path, - 'data': data, - }); + }) => + channel.invokeMethod('Transaction#set', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + 'data': data, + }); // Write Batch @override Future createWriteBatch(String app) => channel.invokeMethod( - 'WriteBatch#create', {'app': app}); + 'WriteBatch#create', {'app': app}); @override Future commitWriteBatch({ @required dynamic handle, - }) => channel.invokeMethod( + }) => + channel.invokeMethod( 'WriteBatch#commit', {'handle': handle}); @override - Future deleteWriteBatch(String app, { + Future deleteWriteBatch( + String app, { @required dynamic handle, @required String path, - }) => channel.invokeMethod( - 'WriteBatch#delete', - { - 'app': app, - 'handle': handle, - 'path': path, - }, - ); + }) => + channel.invokeMethod( + 'WriteBatch#delete', + { + 'app': app, + 'handle': handle, + 'path': path, + }, + ); @override - Future setWriteBatchData(String app, { + Future setWriteBatchData( + String app, { @required dynamic handle, @required String path, Map data, Map options, - }) => channel.invokeMethod( - 'WriteBatch#setData', - { - 'app': app, - 'handle': handle, - 'path': path, - 'data': data, - 'options': options, - }, - ); + }) => + channel.invokeMethod( + 'WriteBatch#setData', + { + 'app': app, + 'handle': handle, + 'path': path, + 'data': data, + 'options': options, + }, + ); @override - Future updateWriteBatchData(String app, { + Future updateWriteBatchData( + String app, { @required dynamic handle, @required String path, Map data, - }) => channel.invokeMethod( - 'WriteBatch#updateData', - { - 'app': app, - 'handle': handle, - 'path': path, - 'data': data, - }, - ); + }) => + channel.invokeMethod( + 'WriteBatch#updateData', + { + 'app': app, + 'handle': handle, + 'path': path, + 'data': data, + }, + ); } diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/test/cloud_firestore_platform_interface_test.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/test/cloud_firestore_platform_interface_test.dart index 48602f1f88fe..a032634955e6 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/test/cloud_firestore_platform_interface_test.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/test/cloud_firestore_platform_interface_test.dart @@ -11,7 +11,8 @@ import 'package:mockito/mockito.dart'; void main() { group('$CloudFirestorePlatform', () { test('$MethodChannelCloudFirestore is the default instance', () { - expect(CloudFirestorePlatform.instance, isA()); + expect( + CloudFirestorePlatform.instance, isA()); }); test('Cannot be implemented with `implements`', () { diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_firebase_core_test.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_firebase_core_test.dart index d781a33c3cdc..ce98d5bc0831 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_firebase_core_test.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_firebase_core_test.dart @@ -14,8 +14,5 @@ void main() { group('$MethodChannelCloudFirestore', () { final MethodChannelCloudFirestore channelPlatform = MethodChannelCloudFirestore(); - - - }); } From 69906eec7e453aa72886075c4af22fe032b75548 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 6 Dec 2019 13:55:14 -0800 Subject: [PATCH 14/16] [cloud_firestore_platform_interface] Address PR feedback --- .../src/method_channel_cloud_firestore.dart | 316 ++++++++++-------- 1 file changed, 169 insertions(+), 147 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart index 37de25474f24..c0ec9d8f7e8f 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart @@ -36,18 +36,22 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { // Global @override - Future removeListener(int handle) => channel.invokeMethod( - 'removeListener', - {'handle': handle}, - ); + Future removeListener(int handle) { + return channel.invokeMethod( + 'removeListener', + {'handle': handle}, + ); + } // Firestore @override - Future enablePersistence(String app, {bool enable = true}) => channel - .invokeMethod('Firestore#enablePersistence', { - 'app': app, - 'enable': enable, - }); + Future enablePersistence(String app, {bool enable = true}) { + return channel + .invokeMethod('Firestore#enablePersistence', { + 'app': app, + 'enable': enable, + }); + } @override Future settings( @@ -56,14 +60,15 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { String host, bool sslEnabled, int cacheSizeBytes, - }) => - channel.invokeMethod('Firestore#settings', { - 'app': app, - 'persistenceEnabled': persistenceEnabled, - 'host': host, - 'sslEnabled': sslEnabled, - 'cacheSizeBytes': cacheSizeBytes, - }); + }) { + return channel.invokeMethod('Firestore#settings', { + 'app': app, + 'persistenceEnabled': persistenceEnabled, + 'host': host, + 'sslEnabled': sslEnabled, + 'cacheSizeBytes': cacheSizeBytes, + }); + } // Transaction data static final Map _transactionHandlers = @@ -76,7 +81,7 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { @required PlatformTransactionHandler transactionHandler, int transactionTimeout, }) async { - // Store the transaction handler function so we can use it later on DoTransaction... + // The [transactionHandler] will be used by the [_handleDoTransaction] method later final int transactionId = _transactionHandlerId++; _transactionHandlers[transactionId] = transactionHandler; @@ -90,11 +95,11 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { Future _handleDoTransaction(MethodCall call) async { final int transactionId = call.arguments['transactionId']; + // Retrieve the handler passed to [runTransaction]... final PlatformTransactionHandler transactionHandler = _transactionHandlers[transactionId]; - // Delete the handler from the list... _transactionHandlers.remove(transactionId); - // Delegate handling to the implementation + // Delegate handling to it return await transactionHandler(transactionId); } @@ -106,31 +111,33 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { // TODO: Type SetOptions: https://firebase.google.com/docs/reference/js/firebase.firestore.SetOptions.html Map options, Map data = const {}, - }) => - channel.invokeMethod( - 'DocumentReference#setData', - { - 'app': app, - 'path': path, - 'data': data, - 'options': options, - }, - ); + }) { + return channel.invokeMethod( + 'DocumentReference#setData', + { + 'app': app, + 'path': path, + 'data': data, + 'options': options, + }, + ); + } @override Future updateDocumentReferenceData( String app, { @required String path, Map data = const {}, - }) => - channel.invokeMethod( - 'DocumentReference#updateData', - { - 'app': app, - 'path': path, - 'data': data, - }, - ); + }) { + return channel.invokeMethod( + 'DocumentReference#updateData', + { + 'app': app, + 'path': path, + 'data': data, + }, + ); + } // TODO: Type this return @override @@ -138,39 +145,42 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { String app, { @required String path, @required String source, - }) => - channel.invokeMapMethod( - 'DocumentReference#get', - { - 'app': app, - 'path': path, - 'source': source, - }, - ); + }) { + return channel.invokeMapMethod( + 'DocumentReference#get', + { + 'app': app, + 'path': path, + 'source': source, + }, + ); + } @override Future deleteDocumentReference( String app, { @required String path, - }) => - channel.invokeMethod( - 'DocumentReference#delete', - {'app': app, 'path': path}, - ); + }) { + return channel.invokeMethod( + 'DocumentReference#delete', + {'app': app, 'path': path}, + ); + } Future _addDocumentReferenceSnapshotListener( String app, { @required String path, bool includeMetadataChanges, - }) => - channel.invokeMethod( - 'DocumentReference#addSnapshotListener', - { - 'app': app, - 'path': path, - 'includeMetadataChanges': includeMetadataChanges, - }, - ); + }) { + return channel.invokeMethod( + 'DocumentReference#addSnapshotListener', + { + 'app': app, + 'path': path, + 'includeMetadataChanges': includeMetadataChanges, + }, + ); + } static final Map> _documentObservers = >{}; @@ -182,6 +192,7 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { @required String path, bool includeMetadataChanges, }) { + 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. @@ -219,17 +230,18 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { bool isCollectionGroup, Map parameters, bool includeMetadataChanges, - }) => - channel.invokeMethod( - 'Query#addSnapshotListener', - { - 'app': app, - 'path': path, - 'isCollectionGroup': isCollectionGroup, - 'parameters': parameters, - 'includeMetadataChanges': includeMetadataChanges, - }, - ); + }) { + return channel.invokeMethod( + 'Query#addSnapshotListener', + { + 'app': app, + 'path': path, + 'isCollectionGroup': isCollectionGroup, + 'parameters': parameters, + 'includeMetadataChanges': includeMetadataChanges, + }, + ); + } static final Map> _queryObservers = >{}; @@ -242,7 +254,6 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { Map parameters, bool includeMetadataChanges, }) { - // Create a stream of query snapshot handles, as they happen assert(includeMetadataChanges != null); Future _handle; // It's fine to let the StreamController be garbage collected once all the @@ -285,17 +296,18 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { bool isCollectionGroup, Map parameters, String source, - }) => - channel.invokeMapMethod( - 'Query#getDocuments', - { - 'app': app, - 'path': path, - 'isCollectionGroup': isCollectionGroup, - 'parameters': parameters, - 'source': source, - }, - ); + }) { + return channel.invokeMapMethod( + 'Query#getDocuments', + { + 'app': app, + 'path': path, + 'isCollectionGroup': isCollectionGroup, + 'parameters': parameters, + 'source': source, + }, + ); + } // Transaction // TODO: Type this return @@ -304,25 +316,27 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { String app, { @required String path, @required int transactionId, - }) => - channel.invokeMapMethod( - 'Transaction#get', { - 'app': app, - 'transactionId': transactionId, - 'path': path, - }); + }) { + return channel + .invokeMapMethod('Transaction#get', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + }); + } @override Future deleteTransaction( String app, { @required String path, @required int transactionId, - }) => - channel.invokeMethod('Transaction#delete', { - 'app': app, - 'transactionId': transactionId, - 'path': path, - }); + }) { + return channel.invokeMethod('Transaction#delete', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + }); + } @override Future updateTransaction( @@ -330,13 +344,14 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { @required String path, @required int transactionId, Map data, - }) => - channel.invokeMethod('Transaction#update', { - 'app': app, - 'transactionId': transactionId, - 'path': path, - 'data': data, - }); + }) { + return channel.invokeMethod('Transaction#update', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + 'data': data, + }); + } @override Future setTransaction( @@ -344,40 +359,45 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { @required String path, @required int transactionId, Map data, - }) => - channel.invokeMethod('Transaction#set', { - 'app': app, - 'transactionId': transactionId, - 'path': path, - 'data': data, - }); + }) { + return channel.invokeMethod('Transaction#set', { + 'app': app, + 'transactionId': transactionId, + 'path': path, + 'data': data, + }); + } // Write Batch @override - Future createWriteBatch(String app) => channel.invokeMethod( - 'WriteBatch#create', {'app': app}); + Future createWriteBatch(String app) { + return channel.invokeMethod( + 'WriteBatch#create', {'app': app}); + } @override Future commitWriteBatch({ @required dynamic handle, - }) => - channel.invokeMethod( - 'WriteBatch#commit', {'handle': handle}); + }) { + return channel.invokeMethod( + 'WriteBatch#commit', {'handle': handle}); + } @override Future deleteWriteBatch( String app, { @required dynamic handle, @required String path, - }) => - channel.invokeMethod( - 'WriteBatch#delete', - { - 'app': app, - 'handle': handle, - 'path': path, - }, - ); + }) { + return channel.invokeMethod( + 'WriteBatch#delete', + { + 'app': app, + 'handle': handle, + 'path': path, + }, + ); + } @override Future setWriteBatchData( @@ -386,17 +406,18 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { @required String path, Map data, Map options, - }) => - channel.invokeMethod( - 'WriteBatch#setData', - { - 'app': app, - 'handle': handle, - 'path': path, - 'data': data, - 'options': options, - }, - ); + }) { + return channel.invokeMethod( + 'WriteBatch#setData', + { + 'app': app, + 'handle': handle, + 'path': path, + 'data': data, + 'options': options, + }, + ); + } @override Future updateWriteBatchData( @@ -404,14 +425,15 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { @required dynamic handle, @required String path, Map data, - }) => - channel.invokeMethod( - 'WriteBatch#updateData', - { - 'app': app, - 'handle': handle, - 'path': path, - 'data': data, - }, - ); + }) { + return channel.invokeMethod( + 'WriteBatch#updateData', + { + 'app': app, + 'handle': handle, + 'path': path, + 'data': data, + }, + ); + } } From 75ab8e9d4f1a48438219ada8cc429dd5caf16267 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 6 Dec 2019 19:44:09 -0800 Subject: [PATCH 15/16] [cloud_firestore] Some PR nits --- .../cloud_firestore/cloud_firestore/lib/src/firestore.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart index 0c69359bc615..2ce8b6a7de53 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart @@ -89,7 +89,8 @@ class Firestore { assert(timeout.inMilliseconds > 0, 'Transaction timeout must be more than 0 milliseconds'); - // Wrap the transactionHandler into something that can be passed to the Platform implementation + // Wrap the incoming [TransactionHandler] into something that can be passed + // to the Platform implementation. final PlatformTransactionHandler handler = (int transactionId) async { Transaction transaction = Transaction(transactionId, this); final dynamic result = await transactionHandler(transaction); @@ -97,7 +98,6 @@ class Firestore { return result; }; - // Move to a runTransaction method in the method channel! final Map result = await platform.runTransaction( app.name, transactionHandler: handler, From 5bf190edd996163cea111bb000919137a53ea189 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 6 Dec 2019 19:45:24 -0800 Subject: [PATCH 16/16] [cloud_firestore_platform_interface] [wip] Adding tests (from original package) --- .../src/method_channel_cloud_firestore.dart | 2 +- ...oud_firestore_platform_interface_test.dart | 2 + .../method_channel_firebase_core_test.dart | 1211 ++++++++++++++++- 3 files changed, 1211 insertions(+), 4 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart index c0ec9d8f7e8f..3cfd914aa523 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel_cloud_firestore.dart @@ -197,7 +197,7 @@ class MethodChannelCloudFirestore extends CloudFirestorePlatform { // It's fine to let the StreamController be garbage collected once all the // subscribers have cancelled; this analyzer warning is safe to ignore. StreamController controller; // ignore: close_sinks - controller = StreamController.broadcast( + controller = StreamController.broadcast( onListen: () { _handle = _addDocumentReferenceSnapshotListener( app, diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/test/cloud_firestore_platform_interface_test.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/test/cloud_firestore_platform_interface_test.dart index a032634955e6..fbbc5a0189a0 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/test/cloud_firestore_platform_interface_test.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/test/cloud_firestore_platform_interface_test.dart @@ -9,6 +9,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('$CloudFirestorePlatform', () { test('$MethodChannelCloudFirestore is the default instance', () { expect( diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_firebase_core_test.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_firebase_core_test.dart index ce98d5bc0831..a67e26790d2a 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_firebase_core_test.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_firebase_core_test.dart @@ -1,8 +1,9 @@ // Copyright 2019 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; +import 'dart:typed_data'; -import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; import 'package:cloud_firestore_platform_interface/src/method_channel_cloud_firestore.dart'; import 'package:flutter/services.dart'; @@ -12,7 +13,1211 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('$MethodChannelCloudFirestore', () { - final MethodChannelCloudFirestore channelPlatform = - MethodChannelCloudFirestore(); + int mockHandleId = 0; + + String appName = 'testApp'; + + MethodChannelCloudFirestore platform = MethodChannelCloudFirestore(); + + final List log = []; + + const Map kMockDocumentSnapshotData = { + '1': 2 + }; + const Map kMockSnapshotMetadata = { + "hasPendingWrites": false, + "isFromCache": false, + }; + + setUp(() async { + mockHandleId = 0; + + MethodChannelCloudFirestore.channel + .setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'Query#addSnapshotListener': + final int handle = mockHandleId++; + // Wait before sending a message back. + // Otherwise the first request didn't have the time to finish. + Future.delayed(Duration.zero).then((_) { + // TODO(hterkelsen): Remove this when defaultBinaryMessages is in stable. + // https://github.com/flutter/flutter/issues/33446 + // ignore: deprecated_member_use + BinaryMessages.handlePlatformMessage( + MethodChannelCloudFirestore.channel.name, + MethodChannelCloudFirestore.channel.codec.encodeMethodCall( + MethodCall('QuerySnapshot', { + 'app': appName, + 'handle': handle, + 'paths': ["${methodCall.arguments['path']}/0"], + 'documents': [kMockDocumentSnapshotData], + 'metadatas': >[kMockSnapshotMetadata], + 'metadata': kMockSnapshotMetadata, + 'documentChanges': [ + { + 'oldIndex': -1, + 'newIndex': 0, + 'type': 'DocumentChangeType.added', + 'document': kMockDocumentSnapshotData, + 'metadata': kMockSnapshotMetadata, + }, + ], + }), + ), + (_) {}, + ); + }); + return handle; + case 'DocumentReference#addSnapshotListener': + final int handle = mockHandleId++; + // Wait before sending a message back. + // Otherwise the first request didn't have the time to finish. + Future.delayed(Duration.zero).then((_) { + // TODO(hterkelsen): Remove this when defaultBinaryMessages is in stable. + // https://github.com/flutter/flutter/issues/33446 + // ignore: deprecated_member_use + BinaryMessages.handlePlatformMessage( + MethodChannelCloudFirestore.channel.name, + MethodChannelCloudFirestore.channel.codec.encodeMethodCall( + MethodCall('DocumentSnapshot', { + 'handle': handle, + 'path': methodCall.arguments['path'], + 'data': kMockDocumentSnapshotData, + 'metadata': kMockSnapshotMetadata, + }), + ), + (_) {}, + ); + }); + return handle; + case 'Query#getDocuments': + return { + 'paths': ["${methodCall.arguments['path']}/0"], + 'documents': [kMockDocumentSnapshotData], + 'metadatas': >[kMockSnapshotMetadata], + 'metadata': kMockSnapshotMetadata, + 'documentChanges': [ + { + 'oldIndex': -1, + 'newIndex': 0, + 'type': 'DocumentChangeType.added', + 'document': kMockDocumentSnapshotData, + 'metadata': kMockSnapshotMetadata, + }, + ], + }; + case 'DocumentReference#setData': + return true; + case 'DocumentReference#get': + if (methodCall.arguments['path'] == 'foo/bar') { + return { + 'path': 'foo/bar', + 'data': {'key1': 'val1'}, + 'metadata': kMockSnapshotMetadata, + }; + } else if (methodCall.arguments['path'] == 'foo/notExists') { + return { + 'path': 'foo/notExists', + 'data': null, + 'metadata': kMockSnapshotMetadata, + }; + } + throw PlatformException(code: 'UNKNOWN_PATH'); + case 'Firestore#runTransaction': + return {'1': 3}; + case 'Transaction#get': + if (methodCall.arguments['path'] == 'foo/bar') { + return { + 'path': 'foo/bar', + 'data': {'key1': 'val1'}, + 'metadata': kMockSnapshotMetadata, + }; + } else if (methodCall.arguments['path'] == 'foo/notExists') { + return { + 'path': 'foo/notExists', + 'data': null, + 'metadata': kMockSnapshotMetadata, + }; + } + throw PlatformException(code: 'UNKNOWN_PATH'); + case 'Transaction#set': + return null; + case 'Transaction#update': + return null; + case 'Transaction#delete': + return null; + case 'WriteBatch#create': + return 1; + default: + return null; + } + }); + log.clear(); + }); + + test('settings', () async { + await platform.settings( + appName, + persistenceEnabled: true, + host: null, + sslEnabled: true, + cacheSizeBytes: 500000, + ); + + expect(log, [ + isMethodCall('Firestore#settings', arguments: { + 'app': appName, + 'persistenceEnabled': true, + 'host': null, + 'sslEnabled': true, + 'cacheSizeBytes': 500000, + }), + ]); + }); + + group('Transaction', () { + test('runTransaction', () async { + final Map result = await platform.runTransaction( + appName, + transactionHandler: (int id) async {}, + transactionTimeout: 3000); + + expect(log, [ + isMethodCall('Firestore#runTransaction', arguments: { + 'app': appName, + 'transactionId': 0, + 'transactionTimeout': 3000 + }), + ]); + expect(result, equals({'1': 3})); + }); + + test('get', () async { + await platform.getTransaction(appName, + path: 'foo/bar', transactionId: 0); + + expect(log, [ + isMethodCall('Transaction#get', arguments: { + 'app': appName, + 'transactionId': 0, + 'path': 'foo/bar', + }) + ]); + }); + + test('get notExists', () async { + await platform.getTransaction(appName, + path: 'foo/notExists', transactionId: 0); + + expect(log, [ + isMethodCall('Transaction#get', arguments: { + 'app': appName, + 'transactionId': 0, + 'path': 'foo/notExists', + }) + ]); + }); + + test('delete', () async { + await platform.deleteTransaction(appName, + path: 'foo/bar', transactionId: 0); + + expect(log, [ + isMethodCall('Transaction#delete', arguments: { + 'app': appName, + 'transactionId': 0, + 'path': 'foo/bar', + }) + ]); + }); + + test('set', () async { + await platform.setTransaction( + appName, + path: 'foo/bar', + transactionId: 0, + data: {'key1': 'val1', 'key2': 'val2'}, + ); + + expect(log, [ + isMethodCall('Transaction#set', arguments: { + 'app': appName, + 'transactionId': 0, + 'path': 'foo/bar', + 'data': {'key1': 'val1', 'key2': 'val2'} + }) + ]); + }); + + group('CollectionsReference', () { + test('listen', () async { + final dynamic snapshot = await platform.getQuerySnapshots( + appName, + path: 'foo', + includeMetadataChanges: true, + isCollectionGroup: false, + parameters: { + 'where': >[], + 'orderBy': >[], + }, + ).first; + final dynamic document = snapshot.documents[0]; + + expect(document.documentID, equals('0')); + expect(document.reference.path, equals('foo/0')); + expect(document.data, equals(kMockDocumentSnapshotData)); + + // Flush the async removeListener call + await Future.delayed(Duration.zero); + expect(log, [ + isMethodCall( + 'Query#addSnapshotListener', + arguments: { + 'app': appName, + 'path': 'foo', + 'isCollectionGroup': false, + 'parameters': { + 'where': >[], + 'orderBy': >[], + }, + 'includeMetadataChanges': true, + }, + ), + isMethodCall( + 'removeListener', + arguments: {'handle': 0}, + ), + ]); + }); + }); + + group('DocumentReference', () { + test('listen', () async { + final dynamic snapshot = await platform + .getDocumentReferenceSnapshots( + appName, + path: 'path/to/foo', + includeMetadataChanges: true, + ) + .first; + + expect(snapshot.documentID, equals('foo')); + expect(snapshot.reference.path, equals('path/to/foo')); + expect(snapshot.data, equals(kMockDocumentSnapshotData)); + // Flush the async removeListener call + await Future.delayed(Duration.zero); + expect( + log, + [ + isMethodCall( + 'DocumentReference#addSnapshotListener', + arguments: { + 'app': appName, + 'path': 'path/to/foo', + 'includeMetadataChanges': true, + }, + ), + isMethodCall( + 'removeListener', + arguments: {'handle': 0}, + ), + ], + ); + }); + test('set', () async { + await platform.setDocumentReferenceData( + appName, + path: 'foo/bar', + data: {'bazKey': 'quxValue'}, + options: {'merge': false}, + ); + + expect( + log, + [ + isMethodCall( + 'DocumentReference#setData', + arguments: { + 'app': appName, + 'path': 'foo/bar', + 'data': {'bazKey': 'quxValue'}, + 'options': {'merge': false}, + }, + ), + ], + ); + }); + test('merge set', () async { + await platform.setDocumentReferenceData( + appName, + path: 'foo/bar', + data: {'bazKey': 'quxValue'}, + options: {'merge': true}, + ); + + expect( + log, + [ + isMethodCall( + 'DocumentReference#setData', + arguments: { + 'app': appName, + 'path': 'foo/bar', + 'data': {'bazKey': 'quxValue'}, + 'options': {'merge': true}, + }, + ), + ], + ); + }); + test('update', () async { + await platform.updateDocumentReferenceData( + appName, + path: 'foo/bar', + data: {'bazKey': 'quxValue'}, + ); + + expect( + log, + [ + isMethodCall( + 'DocumentReference#updateData', + arguments: { + 'app': appName, + 'path': 'foo/bar', + 'data': {'bazKey': 'quxValue'}, + }, + ), + ], + ); + }); + test('delete', () async { + await platform.deleteDocumentReference( + appName, + path: 'foo/bar', + ); + expect( + log, + equals([ + isMethodCall( + 'DocumentReference#delete', + arguments: { + 'app': appName, + 'path': 'foo/bar', + }, + ), + ]), + ); + }); + test('get', () async { + final dynamic snapshot = await platform.getDocumentReference( + appName, + path: 'foo/bar', + source: 'cache', + ); + + expect( + log, + equals([ + isMethodCall( + 'DocumentReference#get', + arguments: { + 'app': appName, + 'path': 'foo/bar', + 'source': 'cache', + }, + ), + ]), + ); + log.clear(); + + expect(snapshot.reference.path, equals('foo/bar')); + expect(snapshot.data.containsKey('key1'), equals(true)); + expect(snapshot.data['key1'], equals('val1')); + expect(snapshot.exists, isTrue); + + final dynamic snapshot2 = await platform.getDocumentReference( + appName, + path: 'foo/notExists', + source: 'default', + ); + + expect(snapshot2.data, isNull); + expect(snapshot2.exists, isFalse); + expect( + log, + equals([ + isMethodCall( + 'DocumentReference#get', + arguments: { + 'app': appName, + 'path': 'foo/notExists', + 'source': 'default', + }, + ), + ]), + ); + + try { + await platform.getDocumentReference(appName, + path: 'baz', source: null); + } on PlatformException catch (e) { + expect(e.code, equals('UNKNOWN_PATH')); + } + }); + /* + test('collection', () async { + final CollectionReference colRef = + collectionReference.document('bar').collection('baz'); + expect(colRef.path, equals('foo/bar/baz')); + }); + test('parent', () async { + final CollectionReference colRef = + collectionReference.document('bar').collection('baz'); + expect(colRef.parent().documentID, equals('bar')); + }); + */ + }); +/* + group('Query', () { + test('getDocumentsFromCollection', () async { + QuerySnapshot snapshot = + await collectionReference.getDocuments(source: Source.server); + expect(snapshot.metadata.hasPendingWrites, + equals(kMockSnapshotMetadata['hasPendingWrites'])); + expect(snapshot.metadata.isFromCache, + equals(kMockSnapshotMetadata['isFromCache'])); + DocumentSnapshot document = snapshot.documents.first; + expect(document.documentID, equals('0')); + expect(document.reference.path, equals('foo/0')); + expect(document.data, equals(kMockDocumentSnapshotData)); + + // startAtDocument + snapshot = + await collectionReference.startAtDocument(document).getDocuments(); + document = snapshot.documents.first; + expect(document.documentID, equals('0')); + expect(document.reference.path, equals('foo/0')); + expect(document.data, equals(kMockDocumentSnapshotData)); + + // startAfterDocument + snapshot = await collectionReference + .startAfterDocument(document) + .getDocuments(); + document = snapshot.documents.first; + expect(document.documentID, equals('0')); + expect(document.reference.path, equals('foo/0')); + expect(document.data, equals(kMockDocumentSnapshotData)); + + // endAtDocument + snapshot = + await collectionReference.endAtDocument(document).getDocuments(); + document = snapshot.documents.first; + expect(document.documentID, equals('0')); + expect(document.reference.path, equals('foo/0')); + expect(document.data, equals(kMockDocumentSnapshotData)); + + // endBeforeDocument + snapshot = await collectionReference + .endBeforeDocument(document) + .getDocuments(); + document = snapshot.documents.first; + expect(document.documentID, equals('0')); + expect(document.reference.path, equals('foo/0')); + expect(document.data, equals(kMockDocumentSnapshotData)); + + // startAtDocument - endAtDocument + snapshot = await collectionReference + .startAtDocument(document) + .endAtDocument(document) + .getDocuments(); + document = snapshot.documents.first; + expect(document.documentID, equals('0')); + expect(document.reference.path, equals('foo/0')); + expect(document.data, equals(kMockDocumentSnapshotData)); + + expect( + log, + equals( + [ + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'foo', + 'isCollectionGroup': false, + 'source': 'server', + 'parameters': { + 'where': >[], + 'orderBy': >[], + }, + }, + ), + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'foo', + 'isCollectionGroup': false, + 'source': 'default', + 'parameters': { + 'where': >[], + 'orderBy': >[], + 'startAtDocument': { + 'id': '0', + 'path': 'foo/0', + 'data': kMockDocumentSnapshotData, + }, + }, + }, + ), + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'foo', + 'isCollectionGroup': false, + 'source': 'default', + 'parameters': { + 'where': >[], + 'orderBy': >[], + 'startAfterDocument': { + 'id': '0', + 'path': 'foo/0', + 'data': kMockDocumentSnapshotData, + }, + }, + }, + ), + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'foo', + 'isCollectionGroup': false, + 'source': 'default', + 'parameters': { + 'where': >[], + 'orderBy': >[], + 'endAtDocument': { + 'id': '0', + 'path': 'foo/0', + 'data': kMockDocumentSnapshotData, + }, + }, + }, + ), + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'foo', + 'isCollectionGroup': false, + 'source': 'default', + 'parameters': { + 'where': >[], + 'orderBy': >[], + 'endBeforeDocument': { + 'id': '0', + 'path': 'foo/0', + 'data': kMockDocumentSnapshotData, + }, + }, + }, + ), + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'foo', + 'isCollectionGroup': false, + 'source': 'default', + 'parameters': { + 'where': >[], + 'orderBy': >[], + 'startAtDocument': { + 'id': '0', + 'path': 'foo/0', + 'data': kMockDocumentSnapshotData, + }, + 'endAtDocument': { + 'id': '0', + 'path': 'foo/0', + 'data': kMockDocumentSnapshotData, + }, + }, + }, + ), + ], + ), + ); + }); + test('getDocumentsFromCollectionGroup', () async { + QuerySnapshot snapshot = await collectionGroupQuery.getDocuments(); + expect(snapshot.metadata.hasPendingWrites, + equals(kMockSnapshotMetadata['hasPendingWrites'])); + expect(snapshot.metadata.isFromCache, + equals(kMockSnapshotMetadata['isFromCache'])); + DocumentSnapshot document = snapshot.documents.first; + expect(document.documentID, equals('0')); + expect(document.reference.path, equals('bar/0')); + expect(document.data, equals(kMockDocumentSnapshotData)); + + // startAtDocument + snapshot = + await collectionGroupQuery.startAtDocument(document).getDocuments(); + document = snapshot.documents.first; + expect(document.documentID, equals('0')); + expect(document.reference.path, equals('bar/0')); + expect(document.data, equals(kMockDocumentSnapshotData)); + + // startAfterDocument + snapshot = await collectionGroupQuery + .startAfterDocument(document) + .getDocuments(); + document = snapshot.documents.first; + expect(document.documentID, equals('0')); + expect(document.reference.path, equals('bar/0')); + expect(document.data, equals(kMockDocumentSnapshotData)); + + // endAtDocument + snapshot = + await collectionGroupQuery.endAtDocument(document).getDocuments(); + document = snapshot.documents.first; + expect(document.documentID, equals('0')); + expect(document.reference.path, equals('bar/0')); + expect(document.data, equals(kMockDocumentSnapshotData)); + + // endBeforeDocument + snapshot = await collectionGroupQuery + .endBeforeDocument(document) + .getDocuments(); + document = snapshot.documents.first; + expect(document.documentID, equals('0')); + expect(document.reference.path, equals('bar/0')); + expect(document.data, equals(kMockDocumentSnapshotData)); + + // startAtDocument - endAtDocument + snapshot = await collectionGroupQuery + .startAtDocument(document) + .endAtDocument(document) + .getDocuments(); + document = snapshot.documents.first; + expect(document.documentID, equals('0')); + expect(document.reference.path, equals('bar/0')); + expect(document.data, equals(kMockDocumentSnapshotData)); + + expect( + log, + equals( + [ + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'bar', + 'isCollectionGroup': true, + 'parameters': { + 'where': >[], + 'orderBy': >[], + }, + 'source': 'default', + }, + ), + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'bar', + 'isCollectionGroup': true, + 'parameters': { + 'where': >[], + 'orderBy': >[], + 'startAtDocument': { + 'id': '0', + 'path': 'bar/0', + 'data': kMockDocumentSnapshotData, + }, + }, + 'source': 'default', + }, + ), + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'bar', + 'isCollectionGroup': true, + 'parameters': { + 'where': >[], + 'orderBy': >[], + 'startAfterDocument': { + 'id': '0', + 'path': 'bar/0', + 'data': kMockDocumentSnapshotData, + }, + }, + 'source': 'default', + }, + ), + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'bar', + 'isCollectionGroup': true, + 'parameters': { + 'where': >[], + 'orderBy': >[], + 'endAtDocument': { + 'id': '0', + 'path': 'bar/0', + 'data': kMockDocumentSnapshotData, + }, + }, + 'source': 'default', + }, + ), + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'bar', + 'isCollectionGroup': true, + 'source': 'default', + 'parameters': { + 'where': >[], + 'orderBy': >[], + 'endBeforeDocument': { + 'id': '0', + 'path': 'bar/0', + 'data': kMockDocumentSnapshotData, + }, + }, + 'source': 'default', + }, + ), + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'bar', + 'isCollectionGroup': true, + 'source': 'default', + 'parameters': { + 'where': >[], + 'orderBy': >[], + 'startAtDocument': { + 'id': '0', + 'path': 'bar/0', + 'data': kMockDocumentSnapshotData, + }, + 'endAtDocument': { + 'id': '0', + 'path': 'bar/0', + 'data': kMockDocumentSnapshotData, + }, + }, + 'source': 'default', + }, + ), + ], + ), + ); + }); + + test('FieldPath', () async { + await collectionReference + .where(FieldPath.documentId, isEqualTo: 'bar') + .getDocuments(); + expect( + log, + equals([ + isMethodCall( + 'Query#getDocuments', + arguments: { + 'app': app.name, + 'path': 'foo', + 'isCollectionGroup': false, + 'parameters': { + 'where': >[ + [FieldPath.documentId, '==', 'bar'], + ], + 'orderBy': >[], + }, + 'source': 'default', + }, + ), + ]), + ); + }); + test('orderBy assertions', () async { + // Can only order by the same field once. + expect(() { + firestore.collection('foo').orderBy('bar').orderBy('bar'); + }, throwsAssertionError); + // Cannot order by unsupported types. + expect(() { + firestore.collection('foo').orderBy(0); + }, throwsAssertionError); + // Parameters cannot be null. + expect(() { + firestore.collection('foo').orderBy(null); + }, throwsAssertionError); + expect(() { + firestore.collection('foo').orderBy('bar', descending: null); + }, throwsAssertionError); + + // Cannot order by document id when paginating with documents. + final DocumentReference documentReference = + firestore.document('foo/bar'); + final DocumentSnapshot snapshot = await documentReference.get(); + expect(() { + firestore + .collection('foo') + .startAfterDocument(snapshot) + .orderBy(FieldPath.documentId); + }, throwsAssertionError); + }); + test('document pagination FieldPath assertions', () async { + final DocumentReference documentReference = + firestore.document('foo/bar'); + final DocumentSnapshot snapshot = await documentReference.get(); + final Query query = + firestore.collection('foo').orderBy(FieldPath.documentId); + + expect(() { + query.startAfterDocument(snapshot); + }, throwsAssertionError); + expect(() { + query.startAtDocument(snapshot); + }, throwsAssertionError); + expect(() { + query.endAtDocument(snapshot); + }, throwsAssertionError); + expect(() { + query.endBeforeDocument(snapshot); + }, throwsAssertionError); + }); + }); + + group('FirestoreMessageCodec', () { + const MessageCodec codec = FirestoreMessageCodec(); + final DateTime testTime = DateTime(2015, 10, 30, 11, 16); + final Timestamp timestamp = Timestamp.fromDate(testTime); + test('should encode and decode simple messages', () { + _checkEncodeDecode(codec, testTime); + _checkEncodeDecode(codec, timestamp); + _checkEncodeDecode( + codec, const GeoPoint(37.421939, -122.083509)); + _checkEncodeDecode(codec, firestore.document('foo/bar')); + }); + test('should encode and decode composite message', () { + final List message = [ + testTime, + const GeoPoint(37.421939, -122.083509), + firestore.document('foo/bar'), + ]; + _checkEncodeDecode(codec, message); + }); + test('encode and decode blob', () { + final Uint8List bytes = Uint8List(4); + bytes[0] = 128; + final Blob message = Blob(bytes); + _checkEncodeDecode(codec, message); + }); + + test('encode and decode FieldValue', () { + _checkEncodeDecode(codec, FieldValue.arrayUnion([123])); + _checkEncodeDecode(codec, FieldValue.arrayRemove([123])); + _checkEncodeDecode(codec, FieldValue.delete()); + _checkEncodeDecode(codec, FieldValue.serverTimestamp()); + _checkEncodeDecode(codec, FieldValue.increment(1.0)); + _checkEncodeDecode(codec, FieldValue.increment(1)); + }); + + test('encode and decode FieldPath', () { + _checkEncodeDecode(codec, FieldPath.documentId); + }); + }); + + group('Timestamp', () { + test('is accurate for dates after epoch', () { + final DateTime date = DateTime.fromMillisecondsSinceEpoch(22501); + final Timestamp timestamp = Timestamp.fromDate(date); + + expect(timestamp.seconds, equals(22)); + expect(timestamp.nanoseconds, equals(501000000)); + }); + + test('is accurate for dates before epoch', () { + final DateTime date = DateTime.fromMillisecondsSinceEpoch(-1250); + final Timestamp timestamp = Timestamp.fromDate(date); + + expect(timestamp.seconds, equals(-2)); + expect(timestamp.nanoseconds, equals(750000000)); + }); + + test('creates equivalent timestamps regardless of factory', () { + const int kMilliseconds = 22501; + const int kMicroseconds = 22501000; + final DateTime date = + DateTime.fromMicrosecondsSinceEpoch(kMicroseconds); + + final Timestamp timestamp = Timestamp(22, 501000000); + final Timestamp milliTimestamp = + Timestamp.fromMillisecondsSinceEpoch(kMilliseconds); + final Timestamp microTimestamp = + Timestamp.fromMicrosecondsSinceEpoch(kMicroseconds); + final Timestamp dateTimestamp = Timestamp.fromDate(date); + + expect(timestamp, equals(milliTimestamp)); + expect(milliTimestamp, equals(microTimestamp)); + expect(microTimestamp, equals(dateTimestamp)); + }); + + test('correctly compares timestamps', () { + final Timestamp alpha = Timestamp.fromDate(DateTime(2017, 5, 11)); + final Timestamp beta1 = Timestamp.fromDate(DateTime(2018, 2, 19)); + final Timestamp beta2 = Timestamp.fromDate(DateTime(2018, 4, 2)); + final Timestamp beta3 = Timestamp.fromDate(DateTime(2018, 4, 20)); + final Timestamp preview = Timestamp.fromDate(DateTime(2018, 6, 20)); + final List inOrder = [ + alpha, + beta1, + beta2, + beta3, + preview + ]; + + final List timestamps = [ + beta2, + beta3, + alpha, + preview, + beta1 + ]; + timestamps.sort(); + expect(_deepEqualsList(timestamps, inOrder), isTrue); + }); + + test('rejects dates outside RFC 3339 range', () { + final List invalidDates = [ + DateTime.fromMillisecondsSinceEpoch(-70000000000000), + DateTime.fromMillisecondsSinceEpoch(300000000000000), + ]; + + invalidDates.forEach((DateTime date) { + expect(() => Timestamp.fromDate(date), throwsArgumentError); + }); + }); + }); + + group('WriteBatch', () { + test('set', () async { + final WriteBatch batch = firestore.batch(); + batch.setData( + collectionReference.document('bar'), + {'bazKey': 'quxValue'}, + ); + await batch.commit(); + expect( + log, + [ + isMethodCall('WriteBatch#create', arguments: { + 'app': app.name, + }), + isMethodCall( + 'WriteBatch#setData', + arguments: { + 'app': app.name, + 'handle': 1, + 'path': 'foo/bar', + 'data': {'bazKey': 'quxValue'}, + 'options': {'merge': false}, + }, + ), + isMethodCall( + 'WriteBatch#commit', + arguments: { + 'handle': 1, + }, + ), + ], + ); + }); + test('merge set', () async { + final WriteBatch batch = firestore.batch(); + batch.setData( + collectionReference.document('bar'), + {'bazKey': 'quxValue'}, + merge: true, + ); + await batch.commit(); + expect( + log, + [ + isMethodCall('WriteBatch#create', arguments: { + 'app': app.name, + }), + isMethodCall('WriteBatch#setData', arguments: { + 'app': app.name, + 'handle': 1, + 'path': 'foo/bar', + 'data': {'bazKey': 'quxValue'}, + 'options': {'merge': true}, + }), + isMethodCall( + 'WriteBatch#commit', + arguments: { + 'handle': 1, + }, + ), + ], + ); + }); + test('update', () async { + final WriteBatch batch = firestore.batch(); + batch.updateData( + collectionReference.document('bar'), + {'bazKey': 'quxValue'}, + ); + await batch.commit(); + expect( + log, + [ + isMethodCall( + 'WriteBatch#create', + arguments: { + 'app': app.name, + }, + ), + isMethodCall( + 'WriteBatch#updateData', + arguments: { + 'app': app.name, + 'handle': 1, + 'path': 'foo/bar', + 'data': {'bazKey': 'quxValue'}, + }, + ), + isMethodCall( + 'WriteBatch#commit', + arguments: { + 'handle': 1, + }, + ), + ], + ); + }); + test('delete', () async { + final WriteBatch batch = firestore.batch(); + batch.delete(collectionReference.document('bar')); + await batch.commit(); + expect( + log, + [ + isMethodCall( + 'WriteBatch#create', + arguments: { + 'app': app.name, + }, + ), + isMethodCall( + 'WriteBatch#delete', + arguments: { + 'app': app.name, + 'handle': 1, + 'path': 'foo/bar', + }, + ), + isMethodCall( + 'WriteBatch#commit', + arguments: { + 'handle': 1, + }, + ), + ], + ); + }); + }); + */ + }); }); } + +/* +void _checkEncodeDecode(MessageCodec codec, T message) { + final ByteData encoded = codec.encodeMessage(message); + final T decoded = codec.decodeMessage(encoded); + if (message == null) { + expect(encoded, isNull); + expect(decoded, isNull); + } else { + expect(_deepEquals(message, decoded), isTrue); + final ByteData encodedAgain = codec.encodeMessage(decoded); + expect( + encodedAgain.buffer.asUint8List(), + orderedEquals(encoded.buffer.asUint8List()), + ); + } +} + +bool _deepEquals(dynamic valueA, dynamic valueB) { + if (valueA is TypedData) + return valueB is TypedData && _deepEqualsTypedData(valueA, valueB); + if (valueA is List) return valueB is List && _deepEqualsList(valueA, valueB); + if (valueA is Map) return valueB is Map && _deepEqualsMap(valueA, valueB); + if (valueA is double && valueA.isNaN) return valueB is double && valueB.isNaN; + if (valueA is FieldValue) { + return valueB is FieldValue && _deepEqualsFieldValue(valueA, valueB); + } + if (valueA is FieldPath) + return valueB is FieldPath && valueA.type == valueB.type; + return valueA == valueB; +} + +bool _deepEqualsTypedData(TypedData valueA, TypedData valueB) { + if (valueA is ByteData) { + return valueB is ByteData && + _deepEqualsList( + valueA.buffer.asUint8List(), valueB.buffer.asUint8List()); + } + if (valueA is Uint8List) + return valueB is Uint8List && _deepEqualsList(valueA, valueB); + if (valueA is Int32List) + return valueB is Int32List && _deepEqualsList(valueA, valueB); + if (valueA is Int64List) + return valueB is Int64List && _deepEqualsList(valueA, valueB); + if (valueA is Float64List) + return valueB is Float64List && _deepEqualsList(valueA, valueB); + throw 'Unexpected typed data: $valueA'; +} + +bool _deepEqualsList(List valueA, List valueB) { + if (valueA.length != valueB.length) return false; + for (int i = 0; i < valueA.length; i++) { + if (!_deepEquals(valueA[i], valueB[i])) return false; + } + return true; +} + +bool _deepEqualsMap( + Map valueA, Map valueB) { + if (valueA.length != valueB.length) return false; + for (final dynamic key in valueA.keys) { + if (!valueB.containsKey(key) || !_deepEquals(valueA[key], valueB[key])) + return false; + } + return true; +} + +bool _deepEqualsFieldValue(FieldValue valueA, FieldValue valueB) { + if (valueA.type != valueB.type) return false; + if (valueA.value == null) return valueB.value == null; + if (valueA.value is List) return _deepEqualsList(valueA.value, valueB.value); + if (valueA.value is Map) return _deepEqualsMap(valueA.value, valueB.value); + return valueA.value == valueB.value; +} +*/