[google_maps_flutter_platform_interface] Add BitmapDescriptor.fromJson constructor. (#3263)

This is required so serialized descriptors can be synchronously re-hydrated in the Web implementation. This will be removed/deprecated once the buildWidget API gets refactored.
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md
index dc8eddf..0586ac4 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.0.5
+
+* Temporarily add a `fromJson` constructor to `BitmapDescriptor` so serialized descriptors can be synchronously re-hydrated. This will be removed when a fix for [this issue](https://github.com/flutter/flutter/issues/70330) lands.
+
 ## 1.0.4
 
 * Add a `dispose` method to the interface, so implementations may cleanup resources acquired on `init`.
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart
index a6fdcc1..e10481e 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart
@@ -17,6 +17,18 @@
 class BitmapDescriptor {
   const BitmapDescriptor._(this._json);
 
+  static const String _defaultMarker = 'defaultMarker';
+  static const String _fromAsset = 'fromAsset';
+  static const String _fromAssetImage = 'fromAssetImage';
+  static const String _fromBytes = 'fromBytes';
+
+  static const Set<String> _validTypes = {
+    _defaultMarker,
+    _fromAsset,
+    _fromAssetImage,
+    _fromBytes,
+  };
+
   /// Convenience hue value representing red.
   static const double hueRed = 0.0;
 
@@ -49,14 +61,14 @@
 
   /// Creates a BitmapDescriptor that refers to the default marker image.
   static const BitmapDescriptor defaultMarker =
-      BitmapDescriptor._(<dynamic>['defaultMarker']);
+      BitmapDescriptor._(<dynamic>[_defaultMarker]);
 
   /// Creates a BitmapDescriptor that refers to a colorization of the default
   /// marker image. For convenience, there is a predefined set of hue values.
   /// See e.g. [hueYellow].
   static BitmapDescriptor defaultMarkerWithHue(double hue) {
     assert(0.0 <= hue && hue < 360.0);
-    return BitmapDescriptor._(<dynamic>['defaultMarker', hue]);
+    return BitmapDescriptor._(<dynamic>[_defaultMarker, hue]);
   }
 
   /// Creates a BitmapDescriptor using the name of a bitmap image in the assets
@@ -67,9 +79,9 @@
   @Deprecated("Use fromAssetImage instead")
   static BitmapDescriptor fromAsset(String assetName, {String package}) {
     if (package == null) {
-      return BitmapDescriptor._(<dynamic>['fromAsset', assetName]);
+      return BitmapDescriptor._(<dynamic>[_fromAsset, assetName]);
     } else {
-      return BitmapDescriptor._(<dynamic>['fromAsset', assetName, package]);
+      return BitmapDescriptor._(<dynamic>[_fromAsset, assetName, package]);
     }
   }
 
@@ -89,7 +101,7 @@
   }) async {
     if (!mipmaps && configuration.devicePixelRatio != null) {
       return BitmapDescriptor._(<dynamic>[
-        'fromAssetImage',
+        _fromAssetImage,
         assetName,
         configuration.devicePixelRatio,
       ]);
@@ -99,7 +111,7 @@
     final AssetBundleImageKey assetBundleImageKey =
         await assetImage.obtainKey(configuration);
     return BitmapDescriptor._(<dynamic>[
-      'fromAssetImage',
+      _fromAssetImage,
       assetBundleImageKey.name,
       assetBundleImageKey.scale,
       if (kIsWeb && configuration?.size != null)
@@ -113,7 +125,50 @@
   /// Creates a BitmapDescriptor using an array of bytes that must be encoded
   /// as PNG.
   static BitmapDescriptor fromBytes(Uint8List byteData) {
-    return BitmapDescriptor._(<dynamic>['fromBytes', byteData]);
+    return BitmapDescriptor._(<dynamic>[_fromBytes, byteData]);
+  }
+
+  /// The inverse of .toJson.
+  // This is needed in Web to re-hydrate BitmapDescriptors that have been
+  // transformed to JSON for transport.
+  // TODO(https://github.com/flutter/flutter/issues/70330): Clean this up.
+  BitmapDescriptor.fromJson(dynamic json) : _json = json {
+    assert(_validTypes.contains(_json[0]));
+    switch (_json[0]) {
+      case _defaultMarker:
+        assert(_json.length <= 2);
+        if (_json.length == 2) {
+          assert(_json[1] is num);
+          assert(0 <= _json[1] && _json[1] < 360);
+        }
+        break;
+      case _fromBytes:
+        assert(_json.length == 2);
+        assert(_json[1] != null && _json[1] is List<int>);
+        assert((_json[1] as List).isNotEmpty);
+        break;
+      case _fromAsset:
+        assert(_json.length <= 3);
+        assert(_json[1] != null && _json[1] is String);
+        assert((_json[1] as String).isNotEmpty);
+        if (_json.length == 3) {
+          assert(_json[2] != null && _json[2] is String);
+          assert((_json[2] as String).isNotEmpty);
+        }
+        break;
+      case _fromAssetImage:
+        assert(_json.length <= 4);
+        assert(_json[1] != null && _json[1] is String);
+        assert((_json[1] as String).isNotEmpty);
+        assert(_json[2] != null && _json[2] is double);
+        if (_json.length == 4) {
+          assert(_json[3] != null && _json[3] is List);
+          assert((_json[3] as List).length == 2);
+        }
+        break;
+      default:
+        break;
+    }
   }
 
   final dynamic _json;
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml
index fd3a1c4..a2b5ff5 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml
@@ -3,7 +3,7 @@
 homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_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.4
+version: 1.0.5
 
 dependencies:
   flutter:
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart
new file mode 100644
index 0000000..afcf57d
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart
@@ -0,0 +1,167 @@
+// Copyright 2017 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:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  group('$BitmapDescriptor', () {
+    test('toJson / fromJson', () {
+      final descriptor =
+          BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan);
+      final json = descriptor.toJson();
+
+      // Rehydrate a new bitmap descriptor...
+      // ignore: deprecated_member_use_from_same_package
+      final descriptorFromJson = BitmapDescriptor.fromJson(json);
+
+      expect(descriptorFromJson, isNot(descriptor)); // New instance
+      expect(identical(descriptorFromJson.toJson(), json), isTrue); // Same JSON
+    });
+
+    group('fromJson validation', () {
+      group('type validation', () {
+        test('correct type', () {
+          expect(BitmapDescriptor.fromJson(['defaultMarker']),
+              isA<BitmapDescriptor>());
+        });
+        test('wrong type', () {
+          expect(() {
+            BitmapDescriptor.fromJson(['bogusType']);
+          }, throwsAssertionError);
+        });
+      });
+      group('defaultMarker', () {
+        test('hue is null', () {
+          expect(BitmapDescriptor.fromJson(['defaultMarker']),
+              isA<BitmapDescriptor>());
+        });
+        test('hue is number', () {
+          expect(BitmapDescriptor.fromJson(['defaultMarker', 158]),
+              isA<BitmapDescriptor>());
+        });
+        test('hue is not number', () {
+          expect(() {
+            BitmapDescriptor.fromJson(['defaultMarker', 'nope']);
+          }, throwsAssertionError);
+        });
+        test('hue is out of range', () {
+          expect(() {
+            BitmapDescriptor.fromJson(['defaultMarker', -1]);
+          }, throwsAssertionError);
+          expect(() {
+            BitmapDescriptor.fromJson(['defaultMarker', 361]);
+          }, throwsAssertionError);
+        });
+      });
+      group('fromBytes', () {
+        test('with bytes', () {
+          expect(
+              BitmapDescriptor.fromJson([
+                'fromBytes',
+                Uint8List.fromList([1, 2, 3])
+              ]),
+              isA<BitmapDescriptor>());
+        });
+        test('without bytes', () {
+          expect(() {
+            BitmapDescriptor.fromJson(['fromBytes', null]);
+          }, throwsAssertionError);
+          expect(() {
+            BitmapDescriptor.fromJson(['fromBytes', []]);
+          }, throwsAssertionError);
+        });
+      });
+      group('fromAsset', () {
+        test('name is passed', () {
+          expect(BitmapDescriptor.fromJson(['fromAsset', 'some/path.png']),
+              isA<BitmapDescriptor>());
+        });
+        test('name cannot be null or empty', () {
+          expect(() {
+            BitmapDescriptor.fromJson(['fromAsset', null]);
+          }, throwsAssertionError);
+          expect(() {
+            BitmapDescriptor.fromJson(['fromAsset', '']);
+          }, throwsAssertionError);
+        });
+        test('package is passed', () {
+          expect(
+              BitmapDescriptor.fromJson(
+                  ['fromAsset', 'some/path.png', 'some_package']),
+              isA<BitmapDescriptor>());
+        });
+        test('package cannot be null or empty', () {
+          expect(() {
+            BitmapDescriptor.fromJson(['fromAsset', 'some/path.png', null]);
+          }, throwsAssertionError);
+          expect(() {
+            BitmapDescriptor.fromJson(['fromAsset', 'some/path.png', '']);
+          }, throwsAssertionError);
+        });
+      });
+      group('fromAssetImage', () {
+        test('name and dpi passed', () {
+          expect(
+              BitmapDescriptor.fromJson(
+                  ['fromAssetImage', 'some/path.png', 1.0]),
+              isA<BitmapDescriptor>());
+        });
+        test('name cannot be null or empty', () {
+          expect(() {
+            BitmapDescriptor.fromJson(['fromAssetImage', null, 1.0]);
+          }, throwsAssertionError);
+          expect(() {
+            BitmapDescriptor.fromJson(['fromAssetImage', '', 1.0]);
+          }, throwsAssertionError);
+        });
+        test('dpi must be number', () {
+          expect(() {
+            BitmapDescriptor.fromJson(
+                ['fromAssetImage', 'some/path.png', null]);
+          }, throwsAssertionError);
+          expect(() {
+            BitmapDescriptor.fromJson(
+                ['fromAssetImage', 'some/path.png', 'one']);
+          }, throwsAssertionError);
+        });
+        test('with optional [width, height] List', () {
+          expect(
+              BitmapDescriptor.fromJson([
+                'fromAssetImage',
+                'some/path.png',
+                1.0,
+                [640, 480]
+              ]),
+              isA<BitmapDescriptor>());
+        });
+        test(
+            'optional [width, height] List cannot be null or not contain 2 elements',
+            () {
+          expect(() {
+            BitmapDescriptor.fromJson(
+                ['fromAssetImage', 'some/path.png', 1.0, null]);
+          }, throwsAssertionError);
+          expect(() {
+            BitmapDescriptor.fromJson(
+                ['fromAssetImage', 'some/path.png', 1.0, []]);
+          }, throwsAssertionError);
+          expect(() {
+            BitmapDescriptor.fromJson([
+              'fromAssetImage',
+              'some/path.png',
+              1.0,
+              [640, 480, 1024]
+            ]);
+          }, throwsAssertionError);
+        });
+      });
+    });
+  });
+}