Skip to content

Commit 867f500

Browse files
jokerttuaednlaxer
andauthored
fix: error handling for awaitMapReady calls (#353)
* fix: fixed error handling for awaitMapReady calls * docs: update documentation for onViewCreated * tests: add ControllerCompleter --------- Co-authored-by: Alexander Troshkov <[email protected]>
1 parent 794a890 commit 867f500

File tree

9 files changed

+170
-35
lines changed

9 files changed

+170
-35
lines changed

android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsViewMessageHandler.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ class GoogleMapsViewMessageHandler(private val viewRegistry: GoogleMapsViewRegis
4040
}
4141

4242
override fun awaitMapReady(viewId: Long, callback: (Result<Unit>) -> Unit) {
43-
return getView(viewId.toInt()).awaitMapReady(callback)
43+
try {
44+
getView(viewId.toInt()).awaitMapReady(callback)
45+
} catch (e: Throwable) {
46+
callback(Result.failure(e))
47+
}
4448
}
4549

4650
override fun isMyLocationEnabled(viewId: Long): Boolean {

example/integration_test/shared.dart

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ const double startLocationLng = 23.510763;
6161
/// Timeout for tests in seconds.
6262
const int testTimeoutSeconds = 240; // 4 minutes
6363

64+
/// Timeout for controller completer in seconds. This timeout is set to be
65+
/// long as on CI emulator the controller creation can take a while.
66+
const int controllerCompleterTimeoutSeconds = 30;
67+
6468
const NativeAutomatorConfig _nativeAutomatorConfig = NativeAutomatorConfig(
6569
findTimeout: Duration(seconds: 20),
6670
);
@@ -190,8 +194,8 @@ Future<GoogleNavigationViewController> startNavigation(
190194
void Function(CameraPosition)? onCameraIdle,
191195
void Function(CameraPosition)? onCameraStartedFollowingLocation,
192196
void Function(CameraPosition)? onCameraStoppedFollowingLocation}) async {
193-
final Completer<GoogleNavigationViewController> controllerCompleter =
194-
Completer<GoogleNavigationViewController>();
197+
final ControllerCompleter<GoogleNavigationViewController>
198+
controllerCompleter = ControllerCompleter();
195199

196200
await checkLocationDialogAndTosAcceptance($);
197201

@@ -433,8 +437,8 @@ Future<GoogleMapViewController> startMapView(
433437
onRecenterButtonClicked,
434438
void Function(CameraPosition)? onCameraIdle,
435439
}) async {
436-
final Completer<GoogleMapViewController> controllerCompleter =
437-
Completer<GoogleMapViewController>();
440+
final ControllerCompleter<GoogleMapViewController> controllerCompleter =
441+
ControllerCompleter();
438442

439443
//await checkLocationDialogAndTosAcceptance($);
440444

@@ -507,3 +511,35 @@ int? colorToInt(Color? color) {
507511
/// Helper function to build a reason for the test.
508512
String buildReasonForToggle(String toggle, bool result) =>
509513
'set$toggle($result) should update the internal state so that a subsequent call to is$toggle returns $result.';
514+
515+
/// A wrapper for `Completer<T>` to handle timeouts. T must be either
516+
/// [GoogleNavigationViewController] or [GoogleMapViewController].
517+
class ControllerCompleter<T> {
518+
ControllerCompleter()
519+
: assert(
520+
T == GoogleNavigationViewController || T == GoogleMapViewController,
521+
'T must be either GoogleNavigationViewController or GoogleMapViewController',
522+
);
523+
524+
final Completer<T> _completer = Completer<T>();
525+
526+
/// Completes the completer with the provided [value].
527+
void complete(T value) {
528+
_completer.complete(value);
529+
}
530+
531+
/// Returns the future of the completer. This future will complete
532+
/// with the controller value or throw a [TestFailure] if timeout is reached.
533+
Future<T> get future {
534+
return _completer.future.timeout(
535+
const Duration(seconds: controllerCompleterTimeoutSeconds),
536+
onTimeout: () {
537+
fail(
538+
'Controller not created in time, '
539+
'this could happen if view is disposed before onViewCreated '
540+
'is called',
541+
);
542+
},
543+
);
544+
}
545+
}

example/integration_test/t01_initialization_test.dart

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
// For more information about Flutter integration tests, please see
2121
// https://docs.flutter.dev/cookbook/testing/integration/introduction
2222

23-
import 'dart:async';
2423
import 'dart:io';
2524

2625
import 'package:flutter/material.dart';
@@ -183,8 +182,8 @@ void main() {
183182
});
184183

185184
patrol(prefix('Test Maps initialization'), (PatrolIntegrationTester $) async {
186-
final Completer<GoogleNavigationViewController> viewControllerCompleter =
187-
Completer<GoogleNavigationViewController>();
185+
final ControllerCompleter<GoogleNavigationViewController>
186+
viewControllerCompleter = ControllerCompleter();
188187

189188
await checkTermsAndConditionsAcceptance($);
190189
await checkLocationDialogAcceptance($);
@@ -220,7 +219,6 @@ void main() {
220219
GoogleMapsNavigationView(
221220
key: key,
222221
onViewCreated: (GoogleNavigationViewController controller) {
223-
controller.setMyLocationEnabled(true);
224222
viewControllerCompleter.complete(controller);
225223
},
226224
initialCameraPosition: cameraPosition,
@@ -243,6 +241,9 @@ void main() {
243241

244242
final GoogleNavigationViewController controller =
245243
await viewControllerCompleter.future;
244+
245+
await controller.setMyLocationEnabled(true);
246+
246247
final CameraPosition cameraOut = await controller.getCameraPosition();
247248

248249
expect(cameraOut.target.latitude,
@@ -276,8 +277,8 @@ void main() {
276277

277278
patrol(prefix('Test Maps initialization without navigation'),
278279
(PatrolIntegrationTester $) async {
279-
final Completer<GoogleMapViewController> viewControllerCompleter =
280-
Completer<GoogleMapViewController>();
280+
final ControllerCompleter<GoogleMapViewController> viewControllerCompleter =
281+
ControllerCompleter<GoogleMapViewController>();
281282

282283
const CameraPosition cameraPosition =
283284
CameraPosition(target: LatLng(latitude: 65, longitude: 25.5), zoom: 12);
@@ -300,7 +301,6 @@ void main() {
300301
GoogleMapsMapView(
301302
key: key,
302303
onViewCreated: (GoogleMapViewController controller) {
303-
controller.setMyLocationEnabled(true);
304304
viewControllerCompleter.complete(controller);
305305
},
306306
initialCameraPosition: cameraPosition,
@@ -321,6 +321,9 @@ void main() {
321321

322322
final GoogleMapViewController controller =
323323
await viewControllerCompleter.future;
324+
325+
await controller.setMyLocationEnabled(true);
326+
324327
final CameraPosition cameraOut = await controller.getCameraPosition();
325328

326329
expect(cameraOut.target.latitude,

example/integration_test/t04_navigation_ui_test.dart

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,14 @@
2020
// For more information about Flutter integration tests, please see
2121
// https://docs.flutter.dev/cookbook/testing/integration/introduction
2222

23-
import 'dart:async';
24-
2523
import 'package:flutter/material.dart';
2624

2725
import 'shared.dart';
2826

2927
void main() {
3028
patrol('Test enabling navigation UI', (PatrolIntegrationTester $) async {
31-
final Completer<GoogleNavigationViewController> viewControllerCompleter =
32-
Completer<GoogleNavigationViewController>();
29+
final ControllerCompleter<GoogleNavigationViewController>
30+
viewControllerCompleter = ControllerCompleter();
3331

3432
/// For testing NavigationUIEnabledChanged
3533
bool navigationUIisEnabled = false;
@@ -49,7 +47,6 @@ void main() {
4947
GoogleMapsNavigationView(
5048
key: key,
5149
onViewCreated: (GoogleNavigationViewController controller) {
52-
controller.setMyLocationEnabled(true);
5350
viewControllerCompleter.complete(controller);
5451
},
5552
onNavigationUIEnabledChanged: (bool isEnabled) {
@@ -62,6 +59,8 @@ void main() {
6259
final GoogleNavigationViewController viewController =
6360
await viewControllerCompleter.future;
6461

62+
await viewController.setMyLocationEnabled(true);
63+
6564
expect(await viewController.isNavigationUIEnabled(), false,
6665
reason:
6766
'isNavigationUIEnabled should return false when navigation is not yet initialized.');

example/integration_test/t06_map_test.dart

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@
2020
// For more information about Flutter integration tests, please see
2121
// https://docs.flutter.dev/cookbook/testing/integration/introduction
2222

23-
import 'dart:async';
2423
import 'dart:io';
24+
2525
import 'package:flutter/material.dart';
26+
2627
import 'shared.dart';
2728

2829
void main() {
@@ -56,8 +57,8 @@ void main() {
5657
patrol(
5758
'Test platform view creation params',
5859
(PatrolIntegrationTester $) async {
59-
final Completer<GoogleMapViewController> controllerCompleter =
60-
Completer<GoogleMapViewController>();
60+
final ControllerCompleter<GoogleMapViewController> controllerCompleter =
61+
ControllerCompleter<GoogleMapViewController>();
6162

6263
switch (mapTypeVariants.currentValue!) {
6364
case TestMapType.mapView:
@@ -298,8 +299,9 @@ void main() {
298299
(PatrolIntegrationTester $) async {
299300
/// For some reason the functionality works on Android example app, but it doesn't work
300301
/// during the testing. Will skip Android testing for now.
301-
final Completer<GoogleMapViewController> viewControllerCompleter =
302-
Completer<GoogleMapViewController>();
302+
final ControllerCompleter<GoogleMapViewController>
303+
viewControllerCompleter =
304+
ControllerCompleter<GoogleMapViewController>();
303305

304306
await checkLocationDialogAcceptance($);
305307

@@ -391,8 +393,9 @@ void main() {
391393
(PatrolIntegrationTester $) async {
392394
/// For some reason the functionality works on Android example app, but it doesn't work
393395
/// during the testing. Will skip Android testing for now.
394-
final Completer<GoogleMapViewController> viewControllerCompleter =
395-
Completer<GoogleMapViewController>();
396+
final ControllerCompleter<GoogleMapViewController>
397+
viewControllerCompleter =
398+
ControllerCompleter<GoogleMapViewController>();
396399

397400
await checkLocationDialogAcceptance($);
398401

lib/src/google_maps_map_view.dart

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,35 @@ class GoogleMapsMapView extends GoogleMapsBaseMapView {
283283
super.onCameraStartedFollowingLocation,
284284
super.onCameraStoppedFollowingLocation});
285285

286-
/// On view created callback.
286+
/// Callback triggered when the map view is created.
287+
///
288+
/// Provides a [OnMapViewCreatedCallback] for interacting with and
289+
/// controlling the map after initialization.
290+
///
291+
/// To ensure safe usage, wrap controller calls inside a `try-catch` block
292+
/// as native method calls are asynchronous. This prevents exceptions if the
293+
/// view is unmounted before the native message is handled on the platform
294+
/// side.
295+
///
296+
/// Example:
297+
/// ```dart
298+
/// onViewCreated: (controller) async {
299+
/// try {
300+
/// final CameraUpdate cameraUpdate = CameraUpdate.newLatLng(
301+
/// const LatLng(latitude: 12.3456, longitude: 12.3456),
302+
/// );
303+
/// await controller.setCameraPosition(CameraPosition(
304+
/// target: LatLng(37.7749, -122.4194),
305+
/// zoom: 12,
306+
/// ));
307+
/// } on PlatformException catch (exception, stack) {
308+
/// if (exception.code == 'viewNotFound') {
309+
/// // Handle the case when the view is disposed before the call is
310+
/// // handled on the platform side.
311+
/// }
312+
/// }
313+
/// },
314+
/// ```
287315
final OnMapViewCreatedCallback onViewCreated;
288316

289317
/// Creates a [State] for this [GoogleMapsMapView].

lib/src/google_maps_navigation_view.dart

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,35 @@ class GoogleMapsNavigationView extends GoogleMapsBaseMapView {
8888
super.onCameraStartedFollowingLocation,
8989
super.onCameraStoppedFollowingLocation});
9090

91-
/// On view created callback.
91+
/// Callback triggered when the navigation view is created.
92+
///
93+
/// Provides a [GoogleMapsNavigationViewController] for interacting with and
94+
/// controlling the map after initialization.
95+
///
96+
/// To ensure safe usage, wrap controller calls inside a `try-catch` block
97+
/// as native method calls are asynchronous. This prevents exceptions if the
98+
/// view is unmounted before the native message is handled on the platform
99+
/// side.
100+
///
101+
/// Example:
102+
/// ```dart
103+
/// onViewCreated: (controller) async {
104+
/// try {
105+
/// final CameraUpdate cameraUpdate = CameraUpdate.newLatLng(
106+
/// const LatLng(latitude: 12.3456, longitude: 12.3456),
107+
/// );
108+
/// await controller.setCameraPosition(CameraPosition(
109+
/// target: LatLng(37.7749, -122.4194),
110+
/// zoom: 12,
111+
/// ));
112+
/// } on PlatformException catch (exception, stack) {
113+
/// if (exception.code == 'viewNotFound') {
114+
/// // Handle the case when the view is disposed before the call is
115+
/// // handled on the platform side.
116+
/// }
117+
/// }
118+
/// },
119+
/// ```
92120
final OnNavigationViewCreatedCallback onViewCreated;
93121

94122
/// Determines the initial visibility of the navigation UI on map initialization.

lib/src/google_navigation_flutter_android.dart

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,29 @@ class GoogleMapsNavigationAndroid extends GoogleMapsNavigationPlatform {
9292
return AndroidView(
9393
viewType: viewType,
9494
onPlatformViewCreated: (int viewId) async {
95-
onPlatformViewCreated(viewId);
95+
try {
96+
onPlatformViewCreated(viewId);
9697

97-
// On Android the map is initialized asyncronously.
98-
// Wait map to be ready before calling [onMapReady] callback
99-
await viewAPI.awaitMapReady(viewId: viewId);
100-
onMapReady(viewId);
98+
// On Android the map is initialized asyncronously.
99+
// Wait map to be ready before calling [onMapReady] callback
100+
await viewAPI.awaitMapReady(viewId: viewId);
101+
onMapReady(viewId);
102+
} on PlatformException catch (exception, stack) {
103+
if (exception.code == 'viewNotFound') {
104+
// This exeption can happen if the view is disposed before the calls
105+
// are made to the platform side. We can ignore this exception as
106+
// the view is already disposed.
107+
return;
108+
} else {
109+
// Pass other exceptions to the Flutter error handler.
110+
FlutterError.reportError(FlutterErrorDetails(
111+
exception: exception,
112+
stack: stack,
113+
library: 'google_navigation_flutter',
114+
context: ErrorDescription(exception.message ?? ''),
115+
));
116+
}
117+
}
101118
},
102119
gestureRecognizers: initializationOptions.gestureRecognizers,
103120
layoutDirection: initializationOptions.layoutDirection,

lib/src/google_navigation_flutter_ios.dart

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,28 @@ class GoogleMapsNavigationIOS extends GoogleMapsNavigationPlatform {
9494
creationParams: creationParams.encode(),
9595
creationParamsCodec: const StandardMessageCodec(),
9696
onPlatformViewCreated: (int viewId) async {
97-
onPlatformViewCreated(viewId);
97+
try {
98+
onPlatformViewCreated(viewId);
9899

99-
// Wait map to be ready before calling [onMapReady] callback
100-
await viewAPI.awaitMapReady(viewId: viewId);
101-
onMapReady(viewId);
100+
// Wait map to be ready before calling [onMapReady] callback
101+
await viewAPI.awaitMapReady(viewId: viewId);
102+
onMapReady(viewId);
103+
} on PlatformException catch (exception, stack) {
104+
if (exception.code == 'viewNotFound') {
105+
// This exeption can happen if the view is disposed before the calls
106+
// are made to the platform side. We can ignore this exception as
107+
// the view is already disposed.
108+
return;
109+
} else {
110+
// Pass other exceptions to the Flutter error handler.
111+
FlutterError.reportError(FlutterErrorDetails(
112+
exception: exception,
113+
stack: stack,
114+
library: 'google_navigation_flutter',
115+
context: ErrorDescription(exception.message ?? ''),
116+
));
117+
}
118+
}
102119
},
103120
gestureRecognizers: initializationOptions.gestureRecognizers,
104121
layoutDirection: initializationOptions.layoutDirection,

0 commit comments

Comments
 (0)