Skip to content

Commit 091b87d

Browse files
authored
[google_maps_flutter] Add ability to perform Google Maps SDK warmup (#9674)
The Android Google Maps SDK janks when the first map is shown, completely blocking the app thread. This PR introduces `GoogleMapsFlutterAndroid.warmup()` which lets Flutter developers do what Android developers do to mitigate this: a fake show of a Google Map that forces the SDK to initialize. This is typically done at app startup so that the jank is less visible than during, say, a page transition. More info: flutter/flutter#28493 (comment) This PR also introduces injecting `MapsInitializerApi` to the `GoogleMapsFlutterAndroid` constructor, for testing, and adds a basic test for `initializeWithRenderer`, too. Fixes flutter/flutter#28493 ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent c48f889 commit 091b87d

File tree

11 files changed

+383
-88
lines changed

11 files changed

+383
-88
lines changed

packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.18.0
2+
3+
* Adds support for warming up the Google Maps SDK
4+
via `GoogleMapsFlutterAndroid.warmup()`.
5+
16
## 2.17.0
27

38
* Updates `com.google.android.gms:play-services-maps` to 19.2.0.

packages/google_maps_flutter/google_maps_flutter_android/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ the issue in the TLHC mode.
6161
| Heatmap.maximumZoomIntensity | x |
6262
| HeatmapGradient.colorMapSize ||
6363

64+
## Warmup
65+
66+
The first time a map is shown, the Google Maps SDK may briefly block
67+
the main thread, which could cause UI jank.
68+
If you prefer to control when this happens, you can call
69+
`GoogleMapsFlutterAndroid.warmup()` at some point before showing any maps to
70+
pre-warm the SDK. See this plugin's example code for one way of using this API.
71+
6472
[1]: https://pub.dev/packages/google_maps_flutter
6573
[2]: https://flutter.dev/to/endorsed-federated-plugin
6674
[3]: https://docs.flutter.dev/development/platform-integration/android/platform-views

packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapInitializer.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@
55
package io.flutter.plugins.googlemaps;
66

77
import android.content.Context;
8+
import android.util.Log;
89
import androidx.annotation.NonNull;
910
import androidx.annotation.Nullable;
1011
import androidx.annotation.VisibleForTesting;
12+
import com.google.android.gms.maps.MapView;
1113
import com.google.android.gms.maps.MapsInitializer;
1214
import com.google.android.gms.maps.OnMapsSdkInitializedCallback;
1315
import io.flutter.plugin.common.BinaryMessenger;
1416

1517
/** GoogleMaps initializer used to initialize the Google Maps SDK with preferred settings. */
1618
final class GoogleMapInitializer
1719
implements OnMapsSdkInitializedCallback, Messages.MapsInitializerApi {
20+
private static final String TAG = "GoogleMapInitializer";
1821
private final Context context;
1922
private static Messages.Result<Messages.PlatformRendererType> initializationResult;
2023
private boolean rendererInitialized = false;
@@ -41,6 +44,24 @@ public void initializeWithPreferredRenderer(
4144
}
4245
}
4346

47+
@Override
48+
public void warmup() {
49+
Log.i(TAG, "Google Maps warmup started.");
50+
try {
51+
// This creates a fake map view in order to trigger the SDK's
52+
// initialization. For context, see
53+
// https://github.com/flutter/flutter/issues/28493#issuecomment-2919150669.
54+
MapView mv = new MapView(context);
55+
mv.onCreate(null);
56+
mv.onResume();
57+
mv.onPause();
58+
mv.onDestroy();
59+
Log.i(TAG, "Maps warmup complete.");
60+
} catch (Exception e) {
61+
throw new Messages.FlutterError("Could not warm up", e.toString(), null);
62+
}
63+
}
64+
4465
/**
4566
* Initializes map renderer to with preferred renderer type.
4667
*

packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7657,6 +7657,11 @@ public interface MapsInitializerApi {
76577657
*/
76587658
void initializeWithPreferredRenderer(
76597659
@Nullable PlatformRendererType type, @NonNull Result<PlatformRendererType> result);
7660+
/**
7661+
* Attempts to trigger any thread-blocking work the Google Maps SDK normally does when a map is
7662+
* shown for the first time.
7663+
*/
7664+
void warmup();
76607665

76617666
/** The codec used by MapsInitializerApi. */
76627667
static @NonNull MessageCodec<Object> getCodec() {
@@ -7706,6 +7711,29 @@ public void error(Throwable error) {
77067711
channel.setMessageHandler(null);
77077712
}
77087713
}
7714+
{
7715+
BasicMessageChannel<Object> channel =
7716+
new BasicMessageChannel<>(
7717+
binaryMessenger,
7718+
"dev.flutter.pigeon.google_maps_flutter_android.MapsInitializerApi.warmup"
7719+
+ messageChannelSuffix,
7720+
getCodec());
7721+
if (api != null) {
7722+
channel.setMessageHandler(
7723+
(message, reply) -> {
7724+
ArrayList<Object> wrapped = new ArrayList<>();
7725+
try {
7726+
api.warmup();
7727+
wrapped.add(0, null);
7728+
} catch (Throwable exception) {
7729+
wrapped = wrapError(exception);
7730+
}
7731+
reply.reply(wrapped);
7732+
});
7733+
} else {
7734+
channel.setMessageHandler(null);
7735+
}
7736+
}
77097737
}
77107738
}
77117739
/**

packages/google_maps_flutter/google_maps_flutter_android/example/lib/main.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ void main() {
8585

8686
Completer<AndroidMapRenderer?>? _initializedRendererCompleter;
8787

88-
/// Initializes map renderer to the `latest` renderer type.
88+
/// Initializes map renderer to the `latest` renderer type, and calls
89+
/// [GoogleMapsFlutterAndroid.warmup()].
8990
///
9091
/// The renderer must be requested before creating GoogleMap instances,
9192
/// as the renderer can be initialized only once per application context.
@@ -100,11 +101,13 @@ Future<AndroidMapRenderer?> initializeMapRenderer() async {
100101

101102
WidgetsFlutterBinding.ensureInitialized();
102103

103-
final GoogleMapsFlutterPlatform platform = GoogleMapsFlutterPlatform.instance;
104-
unawaited((platform as GoogleMapsFlutterAndroid)
104+
final GoogleMapsFlutterAndroid platform =
105+
GoogleMapsFlutterPlatform.instance as GoogleMapsFlutterAndroid;
106+
unawaited(platform
105107
.initializeWithRenderer(AndroidMapRenderer.latest)
106108
.then((AndroidMapRenderer initializedRenderer) =>
107-
completer.complete(initializedRenderer)));
109+
completer.complete(initializedRenderer))
110+
.then((_) => platform.warmup()));
108111

109112
return completer.future;
110113
}

packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform {
6565
/// Creates a new Android maps implementation instance.
6666
GoogleMapsFlutterAndroid({
6767
@visibleForTesting MapsApi Function(int mapId)? apiProvider,
68-
}) : _apiProvider = apiProvider ?? _productionApiProvider;
68+
@visibleForTesting MapsInitializerApi? initializerApi,
69+
}) : _apiProvider = apiProvider ?? _productionApiProvider,
70+
_initializerApi = initializerApi ?? MapsInitializerApi();
6971

7072
/// Registers the Android implementation of GoogleMapsFlutterPlatform.
7173
static void registerWith() {
@@ -77,6 +79,8 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform {
7779
// A method to create MapsApi instances, which can be overridden for testing.
7880
final MapsApi Function(int mapId) _apiProvider;
7981

82+
final MapsInitializerApi _initializerApi;
83+
8084
/// The per-map handlers for callbacks from the host side.
8185
@visibleForTesting
8286
final Map<int, HostMapMessageHandler> hostMapHandlers =
@@ -532,16 +536,21 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform {
532536
preferredRenderer = null;
533537
}
534538

535-
final MapsInitializerApi hostApi = MapsInitializerApi();
536-
final PlatformRendererType initializedRenderer =
537-
await hostApi.initializeWithPreferredRenderer(preferredRenderer);
539+
final PlatformRendererType initializedRenderer = await _initializerApi
540+
.initializeWithPreferredRenderer(preferredRenderer);
538541

539542
return switch (initializedRenderer) {
540543
PlatformRendererType.latest => AndroidMapRenderer.latest,
541544
PlatformRendererType.legacy => AndroidMapRenderer.legacy,
542545
};
543546
}
544547

548+
/// Attempts to trigger any thread-blocking work
549+
/// the Google Maps SDK normally does when a map is shown for the first time.
550+
Future<void> warmup() async {
551+
await _initializerApi.warmup();
552+
}
553+
545554
Widget _buildView(
546555
int creationId,
547556
PlatformViewCreatedCallback onPlatformViewCreated, {

packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3104,6 +3104,32 @@ class MapsInitializerApi {
31043104
return (pigeonVar_replyList[0] as PlatformRendererType?)!;
31053105
}
31063106
}
3107+
3108+
/// Attempts to trigger any thread-blocking work
3109+
/// the Google Maps SDK normally does when a map is shown for the first time.
3110+
Future<void> warmup() async {
3111+
final String pigeonVar_channelName =
3112+
'dev.flutter.pigeon.google_maps_flutter_android.MapsInitializerApi.warmup$pigeonVar_messageChannelSuffix';
3113+
final BasicMessageChannel<Object?> pigeonVar_channel =
3114+
BasicMessageChannel<Object?>(
3115+
pigeonVar_channelName,
3116+
pigeonChannelCodec,
3117+
binaryMessenger: pigeonVar_binaryMessenger,
3118+
);
3119+
final List<Object?>? pigeonVar_replyList =
3120+
await pigeonVar_channel.send(null) as List<Object?>?;
3121+
if (pigeonVar_replyList == null) {
3122+
throw _createConnectionError(pigeonVar_channelName);
3123+
} else if (pigeonVar_replyList.length > 1) {
3124+
throw PlatformException(
3125+
code: pigeonVar_replyList[0]! as String,
3126+
message: pigeonVar_replyList[1] as String?,
3127+
details: pigeonVar_replyList[2],
3128+
);
3129+
} else {
3130+
return;
3131+
}
3132+
}
31073133
}
31083134

31093135
/// Dummy interface to force generation of the platform view creation params,

packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,10 @@ abstract class MapsInitializerApi {
786786
@async
787787
PlatformRendererType initializeWithPreferredRenderer(
788788
PlatformRendererType? type);
789+
790+
/// Attempts to trigger any thread-blocking work
791+
/// the Google Maps SDK normally does when a map is shown for the first time.
792+
void warmup();
789793
}
790794

791795
/// Dummy interface to force generation of the platform view creation params,

packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: google_maps_flutter_android
22
description: Android implementation of the google_maps_flutter plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
5-
version: 2.17.0
5+
version: 2.18.0
66

77
environment:
88
sdk: ^3.6.0

packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import 'package:mockito/mockito.dart';
1515

1616
import 'google_maps_flutter_android_test.mocks.dart';
1717

18-
@GenerateNiceMocks(<MockSpec<Object>>[MockSpec<MapsApi>()])
18+
@GenerateNiceMocks(
19+
<MockSpec<Object>>[MockSpec<MapsApi>(), MockSpec<MapsInitializerApi>()])
1920
void main() {
2021
TestWidgetsFlutterBinding.ensureInitialized();
2122

@@ -32,6 +33,40 @@ void main() {
3233
expect(GoogleMapsFlutterPlatform.instance, isA<GoogleMapsFlutterAndroid>());
3334
});
3435

36+
test('normal usage does not call MapsInitializerApi', () async {
37+
final MockMapsApi api = MockMapsApi();
38+
final MockMapsInitializerApi initializerApi = MockMapsInitializerApi();
39+
final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid(
40+
apiProvider: (_) => api, initializerApi: initializerApi);
41+
const int mapId = 1;
42+
maps.ensureApiInitialized(mapId);
43+
await maps.init(1);
44+
45+
verifyZeroInteractions(initializerApi);
46+
});
47+
48+
test('initializeWithPreferredRenderer forwards the initialization call',
49+
() async {
50+
final MockMapsApi api = MockMapsApi();
51+
final MockMapsInitializerApi initializerApi = MockMapsInitializerApi();
52+
final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid(
53+
apiProvider: (_) => api, initializerApi: initializerApi);
54+
await maps.initializeWithRenderer(AndroidMapRenderer.latest);
55+
56+
verify(initializerApi
57+
.initializeWithPreferredRenderer(PlatformRendererType.latest));
58+
});
59+
60+
test('warmup forwards the initialization call', () async {
61+
final MockMapsApi api = MockMapsApi();
62+
final MockMapsInitializerApi initializerApi = MockMapsInitializerApi();
63+
final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid(
64+
apiProvider: (_) => api, initializerApi: initializerApi);
65+
await maps.warmup();
66+
67+
verify(initializerApi.warmup());
68+
});
69+
3570
test('init calls waitForMap', () async {
3671
final MockMapsApi api = MockMapsApi();
3772
final GoogleMapsFlutterAndroid maps =

0 commit comments

Comments
 (0)