packages icon indicating copy to clipboard operation
packages copied to clipboard

[google_maps_flutter] Fixes exception when dispose is called while asynchronous update from didUpdateWidget is executed

Open JamesMcIntosh opened this issue 6 months ago • 4 comments

This PR fixes an exception in Google Maps Flutter which occurs when dispose is called while asynchronous code is executed from didUpdateWidget.

https://github.com/flutter/flutter/issues/43785

I've done the work in 2 commits, the first one just used mount checks on after each awaited retrieval of the controller. The second reuses the controller to limit the number of awaited async calls. I am a little unsure of the expected execution order of all the unawaited futures which update the controller in regards to whether dispose could still be called in between their executions. Testing locally I haven't been able to reproduce the exception with a single awaited controller.

I bumped the version 2.12.2 as no feature behaviour has changed. No new tests in this PR, I'm not sure that you can test this change using widget tests. Existing test coverage ensures that the values are still being updated correctly.

Pre-Review Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

[^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.

Code Sample

This sample app can be used to trigger the error, just repeatedly tapping the button at the bottom of the screen to reload the map. I did the testing in Chrome using the google_maps_flutter_web example project and the javascript key in web/index.html

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

void main() {
  runApp(const MapToggleWidget());
}

class MapToggleWidget extends StatefulWidget {
  const MapToggleWidget({Key? key}) : super(key: key);

  @override
  _MapToggleWidgetState createState() => _MapToggleWidgetState();
}

class _MapToggleWidgetState extends State<MapToggleWidget> {
  bool _showMap = true;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Map Toggle Example')),
        body: Column(
          children: [
            Expanded(child: _showMap ? buildGoogleMap() : buildNoMapContainer()),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: ElevatedButton(
                onPressed: () => setState(() => _showMap = !_showMap),
                child: Text(_showMap ? 'Hide Map' : 'Show Map'),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Container buildNoMapContainer() {
    return Container(
      color: Colors.grey[300],
      child: const Center(
        child: Text(
          'Map is hidden',
          style: TextStyle(fontSize: 18),
        ),
      ),
    );
  }

  GoogleMap buildGoogleMap() {
    return const GoogleMap(
      mapType: MapType.satellite,
      initialCameraPosition: CameraPosition(
        target: LatLng(-38.9000, 175.8000),
        zoom: 10.0,
      ),
    );
  }

}

JamesMcIntosh avatar May 08 '25 12:05 JamesMcIntosh

It looks like this pull request may not have tests. Please make sure to add tests or get an explicit test exemption before merging.

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.If you believe this PR qualifies for a test exemption, contact "@test-exemption-reviewer" in the #hackers channel in Discord (don't just cc them here, they won't see it!). The test exemption team is a small volunteer group, so all reviewers should feel empowered to ask for tests, without delegating that responsibility entirely to the test exemption group.

flutter-dashboard[bot] avatar May 08 '25 12:05 flutter-dashboard[bot]

I'm not sure how I would go about writing a test to make sure that the call from async code is not executed if the widget state has been disposed. If it's possible would the execution of the async code being non-deterministic probably result in a flaky test.

JamesMcIntosh avatar May 09 '25 10:05 JamesMcIntosh

If it's possible would the execution of the async code being non-deterministic probably result in a flaky test.

Why would it be non-deterministic in a test? onPlatformViewCreated is public, so you should be able to control when it is called (and thus when awaiting _controller resolves) relative to other methods, by calling it explicitly.

stuartmorgan-g avatar May 09 '25 12:05 stuartmorgan-g

@stuartmorgan-g I've added tests covering both the didUpdateWidget and onPlatformViewCreated calls.

I have also expanded the mount check around the call to onMapCreated inside onPlatformViewCreated as it seems strange to me that you'd want to call it if the widget has been disposed. Do you agree with this or just back it off to only include the call to _updateTileOverlays?

If the mount check around onMapCreated is fine then I'll add a test for it also.

I was also wondering if there is a technical reason the tile overlays are updated only after the controller is available and not set in the initState like the rest of the state?

JamesMcIntosh avatar May 10 '25 11:05 JamesMcIntosh

@tarrinneal Any update on this one? This is causing lot of noise in our bug tracking events manager. Not causing the app to crash, but still very annoying !

wer-mathurin avatar Jul 03 '25 16:07 wer-mathurin

From triage: @JamesMcIntosh are you still planning on updating this PR based on the review feedback?

stuartmorgan-g avatar Jul 15 '25 18:07 stuartmorgan-g

Hi @stuartmorgan-g, Sorry it's been a while, it's on my TODO list for this week. James

JamesMcIntosh avatar Jul 15 '25 22:07 JamesMcIntosh

I receive error in the web version: DartError: Bad state: Cannot add new events after calling close dart-sdk/lib/internal/js_dev_runtime/private/ddc_runtime/errors.dart 288:3 throw dart-sdk/lib/async/broadcast_stream_controller.dart 243:24 add packages/google_maps_flutter_web/src/google_maps_controller.dart 250:7 dart-sdk/lib/async/zone.dart 1609:9 runUnaryGuarded dart-sdk/lib/async/stream_impl.dart 366:5 [_sendData] dart-sdk/lib/async/stream_impl.dart 542:13 perform dart-sdk/lib/async/stream_impl.dart 647:10 handleNext dart-sdk/lib/async/stream_impl.dart 618:7 callback dart-sdk/lib/async/schedule_microtask.dart 40:11 _microtaskLoop dart-sdk/lib/async/schedule_microtask.dart 49:5 _startMicrotaskLoop dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 186:7 DartError: Bad state: Cannot add new events after calling close dart-sdk/lib/internal/js_dev_runtime/private/ddc_runtime/errors.dart 288:3 throw dart-sdk/lib/async/broadcast_stream_controller.dart 243:24 add packages/google_maps_flutter_web/src/google_maps_controller.dart 263:9 dart-sdk/lib/async/zone.dart 1609:9 runUnaryGuarded dart-sdk/lib/async/stream_impl.dart 366:5 [_sendData] dart-sdk/lib/async/stream_impl.dart 542:13 perform dart-sdk/lib/async/stream_impl.dart 647:10 handleNext dart-sdk/lib/async/stream_impl.dart 618:7 callback dart-sdk/lib/async/schedule_microtask.dart 40:11 _microtaskLoop dart-sdk/lib/async/schedule_microtask.dart 49:5 _startMicrotaskLoop dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 186:7 DartError: Bad state: Cannot add new events after calling close dart-sdk/lib/internal/js_dev_runtime/private/ddc_runtime/errors.dart 288:3 throw dart-sdk/lib/async/broadcast_stream_controller.dart 243:24 add packages/google_maps_flutter_web/src/google_maps_controller.dart 271:7 dart-sdk/lib/async/zone.dart 1609:9 runUnaryGuarded dart-sdk/lib/async/stream_impl.dart 366:5 [_sendData] dart-sdk/lib/async/stream_impl.dart 542:13 perform dart-sdk/lib/async/stream_impl.dart 647:10 handleNext dart-sdk/lib/async/stream_impl.dart 618:7 callback dart-sdk/lib/async/schedule_microtask.dart 40:11 _microtaskLoop dart-sdk/lib/async/schedule_microtask.dart 49:5 _startMicrotaskLoop dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 186:7

lukasgarcya avatar Jul 21 '25 16:07 lukasgarcya

About before comment, I tested in web version that need adaptive this code for google_maps_flutter_web to it didn't show the same error.

lukasgarcya avatar Jul 21 '25 17:07 lukasgarcya