jbxbergdev
jbxbergdev

Reputation: 880

Flutter GoogleMap is blank after resuming from background

I'm experiencing the following issue: My Flutter app uses a GoogleMap. The map loads just fine initially. However, if I put the app into the background and resume a while later, the map stays blank. The Google logo still shows, like it happens when the API key isn't specified. My polygon overlay doesn't show up, either.

The behavior is not reliably repruducable. Sometimes, the map loads fine after the app had been in the background for hours, sometimes the map is blank after minutes. So far, I have only seen this behavior on Android.

There are no specific log outputs that indicate an error.

Any ideas how to fix/work around this?

I filed an issue with screenshot here: https://github.com/flutter/flutter/issues/40284

EDIT 1: I was able to reproduce this with a GoogleMap as root widget and also without any polygon/feature overlay. Also, I found that wildly zooming in at some point 'reanimates' the map (suddenly the map becomes visible again). Is this maybe a known issue with the underlying Android Google Maps SDK?

EDIT 2: I found that the map is still reacting (e.g. tap/gesture listeners still trigger). Also, the map isn't really empty, it just becomes translucent, so the screen displays whatever widget is behind the map.

Upvotes: 16

Views: 12074

Answers (11)

Shamsudheen TK
Shamsudheen TK

Reputation: 31311

This solution works for me.

Important points:

  1. extends with WidgetsBindingObserver
  2. initState() - WidgetsBinding.instance.addObserver(this);
  3. setState for _showGoogleMap
  4. GoogleMapController should be declared inside the onMapCreated

Final code:

class _GoogleMapPreviewWidgetState extends State<GoogleMapPreviewWidget> with WidgetsBindingObserver {

  bool _showGoogleMap = true;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      _reloadMap();
    }
  }

  void _reloadMap() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) {
        setState(() {
          _showGoogleMap = true;
        });
      }
    });
    if (mounted) {
      setState(() {
        _showGoogleMap = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return _showGoogleMap
        ? const GoogleMap(
            initialCameraPosition: CameraPosition(
              target: LatLng(37.7749, -122.4194),
              zoom: 15,
            ),
            onMapCreated: (GoogleMapController controller) {
              Completer<GoogleMapController> mapController = Completer();
              mapController.complete(controller);
              //controller.setMapStyle(_darkMapStyle);
            },
            mapType: MapType.normal,
            zoomControlsEnabled: false,
            myLocationButtonEnabled: false,
            myLocationEnabled: false,
            markers: {
              Marker(
                  markerId: const MarkerId("location1"),
                  position: LatLng(37.7749, -122.4194),
                  onTap: () {
                    //tap action
                  }),
            },
            onTap: (LatLng latLng) {
              //tap action
            },
          )
        : const SizedBox.shrink();
  }

Upvotes: 0

Luke Hutchison
Luke Hutchison

Reputation: 9230

I found a simple solution that works reliably. If you assign a unique GlobalKey to your GoogleMap instance, and update the GlobalKey when the app is resumed, then on resume the old (disposed) Maps widget will be replaced with a new one, which is correctly redrawn. You also have to re-create the controller (or at least the Completer for the controller), because Completer that is typically used with the Maps API is already complete (so it can't be completed a second time):

class _CreateEventViewState extends State<_CreateEventView>
    with WidgetsBindingObserver {                               // Add

  late GlobalKey _googleMapWidgetKey;                           // Add
  late Completer<GoogleMapController> _mapControllerCompleter;  // Add

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);                  // Add
    _googleMapWidgetKey = GlobalKey();                          // Add
    _mapControllerCompleter = Completer<GoogleMapController>(); // Add
  }

  // Add:  
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) async {
    if (state == AppLifecycleState.resumed) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (mounted) {
          setState(() {
            _googleMapWidgetKey = GlobalKey();
            _mapControllerCompleter = Completer<GoogleMapController>();
          });
        }
      });
    }
  }    

  @override
  Widget build(BuildContext context) {
    GoogleMap(
      key: _googleMapWidgetKey,                                 // Add
      onMapCreated: _mapControllerCompleter.complete,           // Add
      // ...
    ),
  }
}

Upvotes: 3

makri aymen abderraouf
makri aymen abderraouf

Reputation: 828

I found two solutions for this issue so:

  1. use this code in your main.dart file

    if (defaultTargetPlatform == TargetPlatform.android) { AndroidGoogleMapsFlutter.useAndroidViewSurface = true; }

  2. you can use applifecycle to check if the screen is mounted or not and rebuild googleMap widget as seen below

    void didChangeAppLifecycleState(AppLifecycleState state) async { if (state == AppLifecycleState.resumed) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { isMapMounted = true; }); } }); if (mounted) { setState(() { isMapMounted = false; }); } setState(() {}); } }

then in your widget tree use isMapMounted ? GoogleMap() : SizedBox()

Upvotes: 0

Deekshith Xetty
Deekshith Xetty

Reputation: 337

In my case the map threw a black screen when called setState, the below solution solved my problem.

return SingleChildScrollView(
  physics: const NeverScrollableScrollPhysics(),
  child:SizedBox(
    width: MediaQuery.of(context).size.width,
    height: MediaQuery.of(context).size.height,
    child: //Your GoogleMap here,
  ),
);

Upvotes: -1

Elmar
Elmar

Reputation: 4445

Another simpler way to implement solution with setMapStyle within statefull widget of map widget. No need to change anything else: Import flutter services:

import 'package:flutter/services.dart';

Code:

  SystemChannels.lifecycle.setMessageHandler((msg) {
      if (msg == AppLifecycleState.resumed.toString()) {
        mapController.setMapStyle("[]");
      }
    });

"mapController" here is the instance of Google map controller you named somewhere in your code. In my case it is like this:

  GoogleMapController _mapController;
  GoogleMapController get mapController => _mapController;

Upvotes: 2

benten
benten

Reputation: 790

   if you facing this problem in 2022 also  add this line above your class

class YourClass extends StatefulWidget with WidgetsBindingObserver

Completer<GoogleMapController> controller = Completer();
    
          @override
            void dispose() {
    
              WidgetsBinding.instance!.removeObserver(this);
            
              super.dispose();
            }
              @override
              void didChangeAppLifecycleState(AppLifecycleState state) async {
                  super.didChangeAppLifecycleState(state);
                print('\n\ndidChangeAppLifecycleState');
             
                    if (state == AppLifecycleState.resumed) {
                        final GoogleMapController controller1 = await controller.future;
                        controller1.setMapStyle('[]');
                }
                
              }
        
        
          void initState() {                             
        
            super.initState();
              
            WidgetsBinding.instance!.addObserver(this);
          }

Upvotes: 0

Nandom Kumchi
Nandom Kumchi

Reputation: 239

When dealing with a stateful widget, put the code below in your code as shown below

class MainScreenState extends State<MainScreen> with WidgetsBindingObserver 
 {....


  @override
  void initState() {
     super.initState();
     WidgetsBinding.instance!.addObserver(this);
     ...    // Other codes here 
  }


  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
       if (state == AppLifecycleState.resumed) {
         mapController!.setMapStyle("[]");
       }
    }
 }

Then you can add the code below in the state widget

Upvotes: 3

gsm
gsm

Reputation: 2426

I tried something & it seems to be working!

Step 01, Implement WidgetsBindingObserver for related class's State class as follows, i.e:

class MainScreenState extends State<MainScreen> with WidgetsBindingObserver {....

Step 02, Override didChangeAppLifecycleState method i.e:

@override
  Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
    super.didChangeAppLifecycleState(state);
    switch (state) {
      case AppLifecycleState.inactive:
        print('appLifeCycleState inactive');
        break;
      case AppLifecycleState.resumed:
        print('appLifeCycleState resumed');
        break;
      case AppLifecycleState.paused:
        print('appLifeCycleState paused');
        break;
      case AppLifecycleState.detached:
        print('appLifeCycleState detached');
        break;
    }
  }

Step 03 add this for init state

WidgetsBinding.instance!.addObserver(this);

Step 04 Step 4 should be as follows

//onMapCreated method
  void onMapCreated(GoogleMapController controller) {
    controller.setMapStyle(Utils.mapStyles);
    _controller.complete(controller);
  }
// lifecycle
  @override
  Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
    super.didChangeAppLifecycleState(state);
    switch (state) {
      case AppLifecycleState.inactive:
        print('appLifeCycleState inactive');
        break;
      case AppLifecycleState.resumed:
        **//Add These lines**
        final GoogleMapController controller = await _controller.future;
        onMapCreated(controller);
        print('appLifeCycleState resumed');
        break;
      case AppLifecycleState.paused:
        print('appLifeCycleState paused');
        break;
      case AppLifecycleState.detached:
        print('appLifeCycleState detached');
        break;
    }
  }

Upvotes: 3

MattyBoy
MattyBoy

Reputation: 25

Another temporary fix that doesn't required forking the plugins, building, etc.
Add a didChangeAppLifecycleState implemented via WidgetsBindingObserver to your Widget and make the GoogleMap widget rebuild with a state change.

Upvotes: 0

Mattia Vitturi
Mattia Vitturi

Reputation: 271

I discovered that if you tap a marker or change the style the map re-renders

class TheWidgetThatHasTheMap with WidgetsBindingObserver {

   //...your code

    @override
    void didChangeAppLifecycleState(AppLifecycleState state) {
        if (state == AppLifecycleState.resumed) {
            controller.setMapStyle("[]");
        }
    }
}

Upvotes: 27

jbxbergdev
jbxbergdev

Reputation: 880

Not a solution to the core problem, but I was able to work around this bug by creating a fork of the plugins project and modifying GoogleMapController.java as follows:

@Override
  public void onActivityResumed(Activity activity) {
    if (disposed || activity.hashCode() != registrarActivityHashCode) {
      return;
    }
    mapView.onResume();
    // Workaround for https://github.com/flutter/flutter/issues/40284
    // This apparently forces a re-render of the map.
    if (googleMap != null) {
      googleMap.setMapType(googleMap.getMapType());
    }
  }

Now, on every resume event, the map will be re-rendered.

Upvotes: 3

Related Questions