[google_maps_flutter] Cloud-based map styling support (#3682)

Recreates https://github.com/flutter/plugins/pull/6553 form flutter/plugins which had approvals in-progress. 

Fixes https://github.com/flutter/flutter/issues/67631
diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md
index 1e2f222..079355c 100644
--- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md
+++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.5.0
+
+* Adds implementation for `cloudMapId` parameter to support cloud-based maps styling.
+
 ## 2.4.1
 
 * Adds pub topics to package metadata.
diff --git a/packages/google_maps_flutter/google_maps_flutter/README.md b/packages/google_maps_flutter/google_maps_flutter/README.md
index 6b89baa..b4b956f 100644
--- a/packages/google_maps_flutter/google_maps_flutter/README.md
+++ b/packages/google_maps_flutter/google_maps_flutter/README.md
@@ -61,6 +61,11 @@
 [platform view display modes](https://flutter.dev/docs/development/platform-integration/platform-views).
 For details, see [the Android README](https://pub.dev/packages/google_maps_flutter_android#display-mode).
 
+#### Cloud-based map styling
+
+Cloud-based map styling works on Android only if `AndroidMapRenderer.latest` map renderer has been initialized.
+For details, see [the Android README](https://pub.dev/packages/google_maps_flutter_android#map-renderer).
+
 ### iOS
 
 To set up, specify your API key in the application delegate `ios/Runner/AppDelegate.m`:
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_controller.dart
index 98ea6d9..1353c59 100644
--- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_controller.dart
+++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_controller.dart
@@ -407,6 +407,30 @@
       // TODO(cyanglaz): un-skip the test when we can test this on CI with API key enabled.
       // https://github.com/flutter/flutter/issues/57057
       skip: isAndroid || isWeb);
+
+  testWidgets(
+    'testCloudMapId',
+    (WidgetTester tester) async {
+      final Completer<int> mapIdCompleter = Completer<int>();
+      final Key key = GlobalKey();
+
+      await pumpMap(
+        tester,
+        GoogleMap(
+          key: key,
+          initialCameraPosition: kInitialCameraPosition,
+          onMapCreated: (GoogleMapController controller) {
+            mapIdCompleter.complete(controller.mapId);
+          },
+          cloudMapId: kCloudMapId,
+        ),
+      );
+      await tester.pumpAndSettle();
+
+      // Await mapIdCompleter to finish to make sure map can be created with cloudMapId
+      await mapIdCompleter.future;
+    },
+  );
 }
 
 /// Repeatedly checks an asynchronous value against a test condition.
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/shared.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/shared.dart
index 013565a..126733f 100644
--- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/shared.dart
+++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/shared.dart
@@ -19,6 +19,9 @@
 const CameraPosition kInitialCameraPosition =
     CameraPosition(target: kInitialMapCenter, zoom: kInitialZoomLevel);
 
+// Dummy map ID
+const String kCloudMapId = '000000000000000'; // Dummy map ID.
+
 /// True if the test is running in an iOS device
 final bool isIOS = defaultTargetPlatform == TargetPlatform.iOS;
 
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile b/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile
index b690cc7..b6bc7de 100644
--- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile
+++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile
@@ -1,5 +1,5 @@
-# Uncomment this line to define a global platform for your project
-# platform :ios, '11.0'
+# Global platform version is set to 12 for this example project to support cloud-based maps styling
+platform :ios, '12.0'
 
 # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
 ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart
index 96479e3..a0060e1 100644
--- a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart
+++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart
@@ -2,6 +2,8 @@
 // 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/material.dart';
 import 'package:google_maps_flutter_android/google_maps_flutter_android.dart';
 import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
@@ -10,6 +12,7 @@
 import 'lite_mode.dart';
 import 'map_click.dart';
 import 'map_coordinates.dart';
+import 'map_map_id.dart';
 import 'map_ui.dart';
 import 'marker_icons.dart';
 import 'move_camera.dart';
@@ -39,6 +42,7 @@
   const SnapshotPage(),
   const LiteModePage(),
   const TileOverlayPage(),
+  const MapIdPage(),
 ];
 
 /// MapsDemo is the Main Application.
@@ -75,6 +79,38 @@
       GoogleMapsFlutterPlatform.instance;
   if (mapsImplementation is GoogleMapsFlutterAndroid) {
     mapsImplementation.useAndroidViewSurface = true;
+    initializeMapRenderer();
   }
   runApp(const MaterialApp(home: MapsDemo()));
 }
+
+Completer<AndroidMapRenderer?>? _initializedRendererCompleter;
+
+/// Initializes map renderer to the `latest` renderer type for Android platform.
+///
+/// The renderer must be requested before creating GoogleMap instances,
+/// as the renderer can be initialized only once per application context.
+Future<AndroidMapRenderer?> initializeMapRenderer() async {
+  if (_initializedRendererCompleter != null) {
+    return _initializedRendererCompleter!.future;
+  }
+
+  final Completer<AndroidMapRenderer?> completer =
+      Completer<AndroidMapRenderer?>();
+  _initializedRendererCompleter = completer;
+
+  WidgetsFlutterBinding.ensureInitialized();
+
+  final GoogleMapsFlutterPlatform mapsImplementation =
+      GoogleMapsFlutterPlatform.instance;
+  if (mapsImplementation is GoogleMapsFlutterAndroid) {
+    unawaited(mapsImplementation
+        .initializeWithRenderer(AndroidMapRenderer.latest)
+        .then((AndroidMapRenderer initializedRenderer) =>
+            completer.complete(initializedRenderer)));
+  } else {
+    completer.complete(null);
+  }
+
+  return completer.future;
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_map_id.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_map_id.dart
new file mode 100644
index 0000000..6b90d77
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_map_id.dart
@@ -0,0 +1,140 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: public_member_api_docs
+
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:google_maps_flutter/google_maps_flutter.dart';
+import 'package:google_maps_flutter_android/google_maps_flutter_android.dart';
+import 'main.dart';
+import 'page.dart';
+
+class MapIdPage extends GoogleMapExampleAppPage {
+  const MapIdPage({Key? key})
+      : super(const Icon(Icons.map), 'Cloud-based maps styling', key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return const MapIdBody();
+  }
+}
+
+class MapIdBody extends StatefulWidget {
+  const MapIdBody({super.key});
+
+  @override
+  State<StatefulWidget> createState() => MapIdBodyState();
+}
+
+const LatLng _kMapCenter = LatLng(52.4478, -3.5402);
+
+class MapIdBodyState extends State<MapIdBody> {
+  GoogleMapController? controller;
+
+  Key _key = const Key('mapId#');
+  String? _mapId;
+  final TextEditingController _mapIdController = TextEditingController();
+  AndroidMapRenderer? _initializedRenderer;
+
+  @override
+  void initState() {
+    initializeMapRenderer()
+        .then<void>((AndroidMapRenderer? initializedRenderer) => setState(() {
+              _initializedRenderer = initializedRenderer;
+            }));
+    super.initState();
+  }
+
+  String _getInitializedsRendererType() {
+    switch (_initializedRenderer) {
+      case AndroidMapRenderer.latest:
+        return 'latest';
+      case AndroidMapRenderer.legacy:
+        return 'legacy';
+      case AndroidMapRenderer.platformDefault:
+      case null:
+        break;
+    }
+    return 'unknown';
+  }
+
+  void _setMapId() {
+    setState(() {
+      _mapId = _mapIdController.text;
+
+      // Change key to initialize new map instance for new mapId.
+      _key = Key(_mapId ?? 'mapId#');
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final GoogleMap googleMap = GoogleMap(
+        onMapCreated: _onMapCreated,
+        initialCameraPosition: const CameraPosition(
+          target: _kMapCenter,
+          zoom: 7.0,
+        ),
+        key: _key,
+        cloudMapId: _mapId);
+
+    final List<Widget> columnChildren = <Widget>[
+      Padding(
+        padding: const EdgeInsets.all(10.0),
+        child: Center(
+          child: SizedBox(
+            width: 300.0,
+            height: 200.0,
+            child: googleMap,
+          ),
+        ),
+      ),
+      Padding(
+          padding: const EdgeInsets.all(10.0),
+          child: TextField(
+            controller: _mapIdController,
+            decoration: const InputDecoration(
+              hintText: 'Map Id',
+            ),
+          )),
+      Padding(
+          padding: const EdgeInsets.all(10.0),
+          child: ElevatedButton(
+            onPressed: () => _setMapId(),
+            child: const Text(
+              'Press to use specified map Id',
+            ),
+          )),
+      if (!kIsWeb &&
+          Platform.isAndroid &&
+          _initializedRenderer != AndroidMapRenderer.latest)
+        Padding(
+          padding: const EdgeInsets.all(10.0),
+          child: Text(
+              'On Android, Cloud-based maps styling only works with "latest" renderer.\n\n'
+              'Current initialized renderer is "${_getInitializedsRendererType()}".'),
+        ),
+    ];
+
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.stretch,
+      children: columnChildren,
+    );
+  }
+
+  @override
+  void dispose() {
+    _mapIdController.dispose();
+    super.dispose();
+  }
+
+  void _onMapCreated(GoogleMapController controllerParam) {
+    setState(() {
+      controller = controllerParam;
+    });
+  }
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml
index 3c90433..cced821 100644
--- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml
+++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml
@@ -18,7 +18,7 @@
     # The example app is bundled with the plugin so we use a path dependency on
     # the parent directory to use the current plugin's version.
     path: ../
-  google_maps_flutter_android: ^2.1.10
+  google_maps_flutter_android: ^2.5.0
   google_maps_flutter_platform_interface: ^2.4.0
 
 dev_dependencies:
diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart
index 14fe651..03adf3a 100644
--- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart
+++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart
@@ -125,6 +125,7 @@
     this.onCameraIdle,
     this.onTap,
     this.onLongPress,
+    this.cloudMapId,
   });
 
   /// Callback method for when the map is ready to be used.
@@ -292,6 +293,12 @@
   /// See [WebGestureHandling] for more details.
   final WebGestureHandling? webGestureHandling;
 
+  /// Identifier that's associated with a specific cloud-based map style.
+  ///
+  /// See https://developers.google.com/maps/documentation/get-map-id
+  /// for more details.
+  final String? cloudMapId;
+
   /// Creates a [State] for this [GoogleMap].
   @override
   State createState() => _GoogleMapState();
@@ -548,5 +555,6 @@
     indoorViewEnabled: map.indoorViewEnabled,
     trafficEnabled: map.trafficEnabled,
     buildingsEnabled: map.buildingsEnabled,
+    cloudMapId: map.cloudMapId,
   );
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
index 9fa3fe7..c5dbbb3 100644
--- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
+++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
@@ -2,7 +2,7 @@
 description: A Flutter plugin for integrating Google Maps in iOS and Android applications.
 repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
-version: 2.4.1
+version: 2.5.0
 
 environment:
   sdk: ">=3.0.0 <4.0.0"
@@ -21,8 +21,8 @@
 dependencies:
   flutter:
     sdk: flutter
-  google_maps_flutter_android: ^2.1.10
-  google_maps_flutter_ios: ^2.1.10
+  google_maps_flutter_android: ^2.5.0
+  google_maps_flutter_ios: ^2.3.0
   google_maps_flutter_platform_interface: ^2.4.0
   google_maps_flutter_web: ^0.5.2