Adren echo
Adren echo

Reputation: 13

flutter_map move camera with map controller through BLoC

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

Answers (5)

harrow
harrow

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

Abdur Rehman
Abdur Rehman

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

Maciej Szakacz
Maciej Szakacz

Reputation: 594

If you use BLoC (as a state management) here there is an example how you can do it without controller.

  1. place your FlutterMap widget inside a your custom widget.
  2. While calling this widget use 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

Kishan Kumar
Kishan Kumar

Reputation: 31

Sounds stupid but are you initialising the map controller? Like MapController mapController = MapController();

Upvotes: 0

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

Related Questions