Reputation: 13
I'm building a flutter app using BLoC pattern and flutter_map package. I'd like to move camera to particular position. I'm trying to pass map controller to my bloc structure and move camera from there but im getting an error:
NoSuchMethodError: The getter 'onReady' was called on null.
I'm not sure if this is the right approach.
class HomePage extends StatelessWidget {
const HomePage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
...,
BlocProvider<MapBloc>(
create: (BuildContext context) => MapBloc(mapController: MapController()) // passing map controller
..add(MapDataInit()),
)
],
...
);
}
}
map_bloc.dart
class MapBloc extends Bloc<MapEvent, MapState> {
final MapController mapController;
LocationRepository _locationRepository = LocationRepository();
MapBloc({@required this.mapController});
@override
get initialState => MapDataUninitialized();
@override
Stream<MapState> mapEventToState(MapEvent event) async* {
final currentState = state;
if (event is AddMarker) {
yield MapDataLoaded(
mapController: this.mapController,
markers: [...]);
this.add(MoveCamera(event.latLan)); // not sure about this
}
if (event is MoveCamera) {
mapController.onReady.then((result) { // i'm getting an error here
mapController.move(event.latLan, 15.0);
});
}
}
}
Widget with map
class SelectLocationView extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<MapBloc, MapState>(
builder: (context, state) {
...
if (state is MapDataLoaded) {
return Container(
child: Center(
child: Container(
child: FlutterMap(
mapController: state.mapController, // I'm trying to get previously defined controller
options: MapOptions(...),
layers: [
TileLayerOptions(
urlTemplate:
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: ['a', 'b', 'c']),
MarkerLayerOptions(...),
],
))),
);
}
},
);
}
}
I have no idea why the map controller has a problem with onReady method.
Upvotes: 1
Views: 9449
Reputation: 385
You can also do it with BLOC. Just use a BlocListener
and update the Controller values according to your state, whenever the state changes
Widget build(BuildContext context) {
//create your map controller
MapController mapController = MapController();
...
//Attach a listener that is called when a state changes and control the MapController
return BlocListener<AddFindingScreenBloc, AddFindingScreenState>(
listener: (context, state) {
mapController.move(LatLng(state.latitude, state.longitude), state.zoom);
},
child:
...
FlutterMap(
mapController: mapController
);
Upvotes: 0
Reputation: 535
The above answers are right but I will add one thing, that was a big problem for me as well. How to keep the zoom level according to the user zoom because when you listen for the current location changes and update your camera position according to it then the zoom level is also picking the one that you provide at the start.
for example: you provide the zoom level 13 at the start and you zoom the screen to 16 then when the camera position updates it will bring you again to zoom level 13, and it is repeating after every second which is really annoying, So you have to provide the zoom level dynamically which will change according to the user zoom level.
First Listen to the location stream:
location.onLocationChanged.listen((event) {
final newLatLng = LatLng(event.latitude!, event.longitude!);
// Use mapController to access the move method
// move() method is used for the camera position changing e.g: move(LatLng center, double zoom)
mapController.move(newLatLng , 13); // this will set the zoom level static to 13
// but we want it to be dynamic according to the user zoom level,
// so then use the mapController.zoom property will dynamically adjust your zoom level
mapController.move(newLatLng , mapController.zoom);
});
Upvotes: 0
Reputation: 594
If you use BLoC (as a state management) here there is an example how you can do it without controller
.
FlutterMap
widget inside a your custom widget.key
and wrap it with BlocBuilder.
In result the FlutterMap
widget will be rebuild with new center.Example:
class _MapView extends StatelessWidget {
const _MapView();
@override
Widget build(BuildContext context) {
return BlocBuilder<MapBloc, MapState>(
builder: (context, state) {
return Scaffold(
body: MapWidget(
key: Key('map${state.latitude}${state.longitude}'),
latitude: state.latitude,
longitude: state.longitude,
),
);
},
);
}
}
class MapWidget extends StatelessWidget {
const MapWidget({
super.key,
required this.latitude,
required this.longitude,
});
final double latitude;
final double longitude;
@override
Widget build(BuildContext context) {
final _searchingPoint = LatLng(
latitude,
longitude,
);
return FlutterMap(
options: MapOptions(
center: _searchingPoint,
zoom: 11,
keepAlive: true,
),
);
}
}
Source: GitHub
Upvotes: 0
Reputation: 31
Sounds stupid but are you initialising the map controller? Like MapController mapController = MapController();
Upvotes: 0
Reputation: 2132
I had a similar problem using GetX.
I decided by applying some premises: First, I kept in the Widget (stateless) any and all manipulation of the map because I need to switch between the google plugin and the flutter_map.
Then, in the controller (in your case in BloC), it just triggers the necessary information so that the view receives from it the new location that must be centered, but the view is the one who knows and who centralizes the map and not the BloC.
In short, I had to work with the static variable to keep the singleton reference of the mapController in the application because the "onReady ()" method was only called once and so I kept the bool to control the state of the map whereas, while "onReady "it is not executed, we do not have access to map objects such as" zoom "and" move ".
My sample code:
class FlutterMapWidget extends StatelessWidget {
static MapController _mapController;
bool isMapRead = false;
FlutterMapWidget() {
// create instance only not exists another reference
if(_mapController == null) {
_mapController = MapController();
}
// after onReady, flag local control variable
_mapController.onReady.then((v) {
isMapRead = _mapController.ready;
});
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
_buildMap(),
// add another map components ...
],
);
}
void _gotoLocation(double lat,double long) {
//check if map is ready to move camera...
if(this.isMapRead) {
_mapController.move(LatLng(lat, long), _mapController?.zoom);
}
}
Widget _buildMap(){
return GetBuilder<MapEventsController>( // GetX state control
init: _mapEventsController, // GetX events controller
builder: (value) { // GetX event fire when update state in controller
updatePinOnMap(value.liveUpdate, value.location);
if(value.liveUpdate){
_gotoLocation(value.location.lat, value.location.lng);
}
return FlutterMap(
mapController: _mapController,
options: MapOptions(
center: LatLng(value?.location?.lat, value?.location?.lng),
zoom: 13.0,
plugins: [
],
onTap: _handleTap,
),
layers: [
TileLayerOptions(
urlTemplate:
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
subdomains: ['a', 'b', 'c']
),
MarkerLayerOptions(markers: markers), //markers
],
);
},
);
}
void updatePinOnMap(bool liveUpdate, Location location) async {
if(location == null) {
return;
}
this.markers.clear();
Marker mark = Marker(
point: LatLng(location.lat, location.lng),
anchorPos: anchorPos,
builder: (ctx) =>
GestureDetector(
onTap: () => _configureModalBottomSheet(ctx),
child: Container(
child: Icon(Icons.my_location, size: 28, color: Colors.red,), // FlutterLogo(),
),
),
);
markers.add(mark);
}
void _handleTap(LatLng latlng) {
}
void _configureModalBottomSheet(context) {
}
}
Upvotes: 2