I'm having issues with the GoogleMap flutter plugin, when closing a showModalBottomSheet, getting the correct callback with correct context.
When my map is created, onMapCreated() is called, and then FAB_DrawerMarkers() is run. This gets a list of objects from the SQFlite DB via Provider, converts them to markers, then draws them, when the app is first run. I have a FAB which opens a showModalBottomSheet, which is used as a settings dialog (covering half the screen). When I tap satellite or normal, I can change the GoogleMap MapType and setState() will make sure it redraws the map instantly. I can also change the returned list of objects, filtered, etc.
When I close the showModalBottomSheet (or even when the selection is made), I want the Map to requery the DB and redraw the markers. However, I'm getting a context error where the Provider cannot be found. I've tried showModalBottomSheet .whenComplete(() => {}) and .then((value) {}) to call some form of callback. However, when I try to call _FAB_DrawMarkers(); I find that the widget has not yet been mounted, and so no Provider can be found.
I've given some examples of the state of flow of the variables with print() below. So I'd like:
- To work out how to correctly callback after making a setting change, to force the map to requery the DB (via Provider) using correct context or timing.
- If I can do this without any callback (eg trigger the drawmarkers on setting change) that is fine.
- I've tried to keep things tidy by putting them in separate dart files, and want to make sure this is correctly done.
MAIN.dart >> SectionMain.dart >> SectionSettings - (main app state passed down to children to be able to call setState() )
Main.dart:
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<MyAppStateContainer>(
create: (context) => MyAppStateContainer(),
child: MaterialApp(
home: Builder(
builder: (context) => SectionMain(mainAppStateRef: this)
MyAppStateContainer.dart:
class MyAppStateContainer extends ChangeNotifier {
MyAppStateContainer();
MapType _mapType = MapType.terrain;
String _someValue = "ABC";
String get getSomeValue => _someValue;
}
SectionMain.dart:
SectionSettings sectionSettings = SectionSettings();
class SectionMain extends StatefulWidget {
const SectionMain({Key key, @required this.mainAppStateRef}) : super(key: key);
final State mainAppStateRef;
@override
_SectionMain createState() => _SectionMain();
}
class _SectionMain extends State<SectionMain> {
BuildContext thisMapPageContext;
final Set<Marker> _markers = {};
GoogleMapController mapController;
MapMarkers mapMarkers = MapMarkers();
...
void callbackTest() {
print("************************ callback again");
_FAB_DrawMarkers();
}
void _onMapCreated(GoogleMapController controller) {
mapController = controller;
//Load custom marker images
mapMarkers.init(); //load custom marker images
//trigger draw markers
_FAB_DrawMarkers();
}
@override
Widget build(BuildContext context) {
this.thisMapPageContext = context;
return Scaffold(
appBar: AppBar(
title: TextMy Map'),
backgroundColor: Colors.green[700],
),
//Put in a stack widget so can layer other widgets on top of map widget
body: Stack(
children: <Widget>[
//Builder so we can get a CTX made, and then Provider.of() finds the Provider
Builder(
builder: (context) => GoogleMap(
mapType: Provider.of<MyAppStateContainer>(context).getMapType, <--- works fine
minMaxZoomPreference: MinMaxZoomPreference(5,8),
markers: _markers,
onMapCreated: _onMapCreated,
initialCameraPosition: CameraPosition(
target: _center,
zoom: 6, //11.0,
),
mapToolbarEnabled: false,
myLocationEnabled: true,
onTap: (LatLng location) {
setState(() {
...
});
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Align(
alignment: Alignment.bottomCenter,
child:
Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
Builder(
builder: (context) =>
FloatingActionButton(
child: Icon(Icons.settings, size: 36.0),
backgroundColor: Colors.green,
onPressed: () {
sectionSettings.onSheetShowContents(widget.mainAppStateRef, context);
}),
),
...
Builder(
builder: (context) =>
FloatingActionButton(
onPressed: () {
_FAB_DrawMarkers();
},
materialTapTargetSize: MaterialTapTargetSize.padded,
backgroundColor: Colors.green,
child: const Icon(Icons.pin_drop, size: 36.0),
),
),
...
])),
),
],
),
);
}
void _FAB_DrawMarkers() async {
BuildContext getContext = this.thisMapPageContext;
print("############ PROBLEM HERE:");
print(getContext);
print(this.widget);
print(this);
print(this.mounted);
print("############");
final selectedValue = Provider.of<MyAppStateContainer>(getContext, listen:false).getSomeValue; <--- works first time
db_manager.DatabaseHelper helper = db_manager.DatabaseHelper.instance;
...
}
}
void settingsClosedCallBack() {
print("CallBack");
_SectionMain().callbackTest();
}
SectionSettings.dart:
class SectionSettings {
State MainState;
Future<dynamic> onSheetShowContents(State mainAppState, BuildContext context) {
MainState = mainAppState;
return showModalBottomSheet(
//showBottomSheet(
context: context,
builder: (context) {
return ListView(
padding: EdgeInsets.all(15.0),
children: <Widget>[
ListTile(
title: Text("Map Settings"),
selected: true,
),
...
ChoiceChip(
label: Text("Normal Map"),
selected: Provider.of<MyAppStateContainer>(context,listen: false).getMapType == MapType.terrain,
onSelected: (value) {
Provider.of<MyAppStateContainer>(context,listen: false).setMapType(MapType.terrain);
MainState.setState(() {
});
},
),
ChoiceChip(
label: Text("Satellite Map"),
selected: Provider.of<MyAppStateContainer>(context,listen: false).getMapType == MapType.satellite,
onSelected: (value) {
Provider.of<MyAppStateContainer>(context, listen: false).setMapType(MapType.satellite);
MainState.setState(() {
});
},
),
...
],
);
//}).whenComplete(() => {
}).then((value) {
//TODO Callback...
//TODO - 1 - Try giving the right context
//TODO - 2 - Try updating the map when items are changed, not during close. The widget seems to be unmounted
//callback to trigger redraw if changed/dirty
//print("*** ShowBottomSheetClosed")
//mainAppState.
//context
//mainAppState.setState(() { })
//MainState.setState(() { reloadMapMarkers(ctx); });
settingsClosedCallBack();
//mainAppState.setState(() { settingsClosedCallBack(); })
}
);
}
}
Debug outs:
void _FAB_DrawMarkers() async {
BuildContext getContext = this.thisMapPageContext;
print("############ PROBLEM HERE:");
print(getContext);
print(this.widget);
print(this);
print(this.mounted);
print("############");
_FAB_DrawMarkers() - First run through, called after onMapCreated():
I/flutter (11115): ############ CONTEXT:
I/flutter (11115): SectionMain(state: _SectionMain#d8dda)
I/flutter (11115): SectionMain
I/flutter (11115): _SectionMain#d8dda
I/flutter (11115): true
I/flutter (11115): ############
_FAB_DrawMarkers() - Second run through, after trying a callback from closing BottomSheet:
I/flutter (11115): ############ CONTEXT:
I/flutter (11115): null
I/flutter (11115): null
I/flutter (11115): _SectionMain#2e084(lifecycle state: created, no widget, not mounted)
I/flutter (11115): false
I/flutter (11115): ############
The main error I get is:
[ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: NoSuchMethodError: The getter 'owner' was called on null.
E/flutter (11115): Receiver: null
E/flutter (11115): Tried calling: owner
E/flutter (11115): #0 Object.noSuchMethod (dart:core-patch/object_patch.dart:53:5)
E/flutter (11115): #1 Provider.of (package:provider/src/provider.dart:193:15)
E/flutter (11115): #2 _SectionMain._FAB_DrawMarkers (package:flutter_app/section_main.dart:255:38)
E/flutter (11115): #3 _SectionMain.callbackTest (package:flutter_app/section_main.dart:60:5)
E/flutter (11115): #4 settingsClosedCallBack (package:flutter_app/section_main.dart:344:18)
E/flutter (11115): #5 SectionSettings.onSheetShowContents.<anonymous closure>
where the error is of course pointing to the Provider line:
final selectedValue = Provider.of<MyAppStateContainer>(getContext, listen:false).getSomeValue; I was hoping that the whenCompleted() or then() from the bottomsheet would ensure that the main widget had been mounted before calling these functions. Perhaps that's the problem - that I'm calling them when the sheet has closed but before the main widget has a chance to remount.
I'm fairly new to Flutter, so many thanks for advice you can offer here. I'm sure it's something to do with me incorrectly passing context around, or not developing in the correct declarative way in Flutter.
Many thanks, J