Reputation: 880
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
Reputation: 31311
This solution works for me.
Important points:
WidgetsBindingObserver
WidgetsBinding.instance.addObserver(this);
_showGoogleMap
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
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
Reputation: 828
I found two solutions for this issue so:
use this code in your main.dart file
if (defaultTargetPlatform == TargetPlatform.android) { AndroidGoogleMapsFlutter.useAndroidViewSurface = true; }
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
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
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
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
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
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
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
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
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