Bloc: How to get initial user coordinates from data provider.

Created on 29 Feb 2020  路  14Comments  路  Source: felangel/bloc

Hi I'm just starting with flutter_bloc and I find it great. I have this one thing I don't understand dough.
I've followed your login tutorial and I'm using bloc/repository pattern to get a userLocation stream tu use in my MapScreen.

In void main() an AppStarted() event is sent directly from BlocProvider..

BlocProvider<AuthenticationBloc>(
          create: (context) {
            return AuthenticationBloc(
              userRepository: UserRepository(),
            )..add(AppStarted());

and the resulting state manages navigation to different screens as the HomeScreen( MapScreen in my case).
So far so good.

In MapScreen's BlocProvider I do add an GetLocationStream() event (as done in BlocProvider) ..

BlocProvider<MapBloc>(create: (context) {
          return MapBloc(mapRepository: MapRepository())
            ..add(GetLocationStream());
        }),

that would fire a repository method that starts the location stream. The problem is that that method doesn't fire (no userLocation prints in console) at MapScreen loading so bloc's initial state passes to UI a null value. It does fire instead (coordinates prints in console) when I send the same GetLocationStream() event from a button's onPressed call back.
What is different between BlocProvider and BlocProvider?

If I can't send that GetLocationStream() event from BlocProvider, how would I than get initial coordinates using flutter_bloc? Can you see what I'm certainly doing wrong? Thank you very much.
Here is the code :
main :

void main() {
  BlocSupervisor.delegate = SimpleBlocDelegate();
  WidgetsFlutterBinding.ensureInitialized();
  BlocSupervisor.delegate = SimpleBlocDelegate();
  final UserRepository userRepository = UserRepository();
  final MapRepository mapRepository = MapRepository();
//  final AlertRepository alertRepository = AlertRepository();
  runApp(
    MultiBlocProvider(
      providers: [
        BlocProvider<AuthenticationBloc>(
          create: (context) {
            return AuthenticationBloc(
              userRepository: UserRepository(),
            )..add(AppStarted());
          },
        ),
//        BlocProvider<MapBloc>(
//            create: (context) {
//          return MapBloc(
//            mapRepository: MapRepository(),
//          )..add(GetLocationStream());
//        }),
      ],
      child: Fixit(
        userRepository: userRepository,
        mapRepository: mapRepository,
//            alertRepository: alertRepository
      ),
    ),
  );
}    

MapScreen :

class MapScreen extends StatelessWidget {
  final String name;
  final MapRepository _mapRepository;
  final MapController _mapController;
  MapScreen(
      {Key key, @required this.name, @required MapRepository mapRepository})
      : assert(mapRepository != null),
        _mapRepository = mapRepository,
        _mapController = MapController(),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
//        BlocProvider<AlertBloc>(
//          create: (context) {
//            return AlertBloc(alertRepository: FirebaseAlertRepository())
//              ..add(LoadAlerts());
//          },
//        ),
        BlocProvider<MapBloc>(create: (context) {
          return MapBloc(mapRepository: MapRepository())
            ..add(GetLocationStream());
//            ..add(CenterMap());
        }),
      ],
      child: BlocBuilder<MapBloc, MapState>(
          bloc: MapBloc(mapRepository: _mapRepository),
          builder: (BuildContext context, MapState state) {
            LatLng userLocation = (state as LocationStream).location;
            return Scaffold(
              appBar: AppBar(
                backgroundColor: Colors.transparent,
                elevation: 0,
                title: Text(
                  'Home',
                  style: TextStyle(color: Colors.orangeAccent, fontSize: 40),
                ),
                actions: <Widget>[
                  IconButton(
                    icon: Icon(
                      Icons.exit_to_app,
                      color: Colors.orange,
                      size: 35,
                    ),
                    onPressed: () {
                      BlocProvider.of<AuthenticationBloc>(context).add(
                        LoggedOut(),
                      );
                    },
                  ),
                ],
              ),
              backgroundColor: Colors.white,
              body: SafeArea(
                minimum: EdgeInsets.symmetric(horizontal: 20),
                child: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Container(
                        height: 570,
                        width: 320,
                        child: FlutterMap(
                          options: MapOptions(
                            center:
                                userLocation, //LatLng(_position.latitude, _position.longitude), //
                            minZoom: 10.0,
                            maxZoom: 19.0,
                          ),
                          mapController: _mapController,
                          layers: [
                            //
//        PolygonLayer(polygonOpts, map, stream)
//                    PolygonLayerOptions(
//                      polygons:
//                    ),
                            TileLayerOptions(

                                urlTemplate:
                                    'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
                                subdomains: ['a', 'b', 'c'],
                                keepBuffer: 20),
                            new MarkerLayerOptions(
                              markers: [
                                Marker(
                                  point: userLocation,
                                  height: 200,
                                  width: 200,
                                  builder: (context) => IconButton(
                                    icon: Icon(Icons.location_on),
                                    color: Colors.red,
                                    iconSize: 60,
                                    onPressed: () {
                                      print('icon tapped');
                                    },
                                  ),
                                ),
                              ],
                            ),
                          ],
                        ),
                      ),
                      SizedBox(
                        height: 10,
                      ),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                        children: <Widget>[
                          RaisedButton(
                            shape: RoundedRectangleBorder(
                                borderRadius: BorderRadius.circular(5)),
                            onPressed: () {
                              BlocProvider.of<MapBloc>(context)
                                  .add(GetLocationStream());
                              _mapController.move(userLocation, 16);
                            },
                            color: Colors.red,
                            child: Padding(
                              padding: EdgeInsets.all(8.0),
                              child: Text(
                                'center',
                                style: TextStyle(
                                    color: Colors.white, fontSize: 30),
                              ),
                            ),
                          ),
                          RaisedButton(
                            shape: RoundedRectangleBorder(
                                borderRadius: BorderRadius.circular(5)),
                            onPressed: () {
                              //TODO  this goes actually in a alert icon callbac, here just navigates icons vc
                            },
                            color: Colors.red,
                            child: Padding(
                              padding: EdgeInsets.all(8.0),
                              child: Text(
                                'alert',
                                style: TextStyle(
                                    color: Colors.white, fontSize: 30),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ),
            );
          }),
    );
  }
}

event :

abstract class MapEvent {
  const MapEvent();
  @override
  List<Object> get props => [];
}

class GetLocationStream extends MapEvent {}

State:

abstract class MapState {
  const MapState();
  @override
  List<Object> get props => [];
}

class LocationStream extends MapState {
  final LatLng location;

  const LocationStream(this.location);

  @override
  List<Object> get props => [location];

  @override
  String toString() => 'LocationStream {location: $location}';
}

Bloc:

class MapBloc extends Bloc<MapEvent, MapState> {
  final MapRepository _mapRepository;
//  LatLng location;
  LatLng locationStream;
  StreamSubscription _locationStreamSubscription;

  MapBloc({@required MapRepository mapRepository})
      : assert(mapRepository != null), // || streamSubscription != null),
        _mapRepository = mapRepository;

  MapState get initialState => LocationStream(locationStream);

  @override
  Stream<MapState> mapEventToState(MapEvent event) async* {

    // user location
    if (event is GetLocationStream) {
      print('MapBloc event received : $event');
      yield* _mapGetLocationStreamToState(event);
    }
  }

Stream<MapState> _mapGetLocationStreamToState(
      GetLocationStream event) async* {
    print(
        '_mapGetLocationStreamToState latitute : ${_mapRepository.getLocationStream().latitude}');
    print(
        '_mapGetLocationStreamToState longitute : ${_mapRepository.getLocationStream().longitude}');
    locationStream = LatLng(_mapRepository.getLocationStream().latitude,
        _mapRepository.getLocationStream().longitude);
//    _mapRepository.getLocationStream().;
//    (location) => add(UpdateLocation(location));
//    add(UpdateLocation(locationStream));
    print('_mapGetLocationStreamToState() locationStream is: $locationStream ');
    yield LocationStream(locationStream);
  }

Repository:

class MapRepository {
  bool isTracking = false;
  final locationManager = Geolocator();
  StreamSubscription _positionStreamSubsciption;


  LatLng getLocationStream() {
    print('getLocationStream() called');
    LatLng location;
    LocationOptions locationOptions = LocationOptions(
        accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 0);
    try {
      if (isTracking == true) {
        _positionStreamSubsciption.cancel();
        isTracking = !isTracking;
      } else {
        _positionStreamSubsciption = locationManager
            .getPositionStream(locationOptions)
            .listen((Position position) {
          if (position != null) {
            location = LatLng(position.latitude, position.longitude);
          }
          isTracking = !isTracking;
          print('getLocationStream() location is : $location');
          return location;
        });
      }
    } catch (error) {
      print('startTracking error: $error');
    }
  }

}
question

Most helpful comment

@felangel Beautiful then I finally got the concept. I misunderstood this in your API Reference docs.
Thank you very much for the library. It is actually making my life easier getting into the new programming paradigm, as I'm porting my app from swift to Flutter and get it ready for production.
Cheers.

All 14 comments

The first thing I noticed is that you are trying to use the Map Bloc in the same context that it is created. IIRC this can't be done and you have to have a context between them. One option would be to wrap your BlocBuilder in a BuildContext widget, the other would be to create a sub child widget and a third option would be to lift the creation of the Map Bloc to a parent widget.

Hi @vinnytwice 馃憢
Thanks for opening an issue!

Are you able to share a link to a sample app which illustrates the problem you're having? Thanks! 馃憤

@warriorCoder Thank you for your suggestions and pointing out that I'm wrongly passing the BlocBuilder as the child of MultiBlocProvider.
I appreciate your help very much indeed. Flutter, reactive programming and bloc/repository pattern are so new to me coming from iOS that I'm sure I don't have some basic concept under my belt.
I think I tried your third suggestion "lift the creation of the Map Bloc to a parent widget" as my first attempt but I'm trying again taking into account my error.
I now create MapBloc inside main()'s MultiBlocProvider() and in AppScreen's build i just return BlocBuilder<MapBloc, MapState> .

main():

runApp(
    MultiBlocProvider(
      providers: [
        BlocProvider<AuthenticationBloc>(
          create: (context) {
            return AuthenticationBloc(
              userRepository: UserRepository(),
            )..add(AppStarted());
          },
        ),
        BlocProvider<MapBloc>(create: (context) {
          return MapBloc(
            mapRepository: MapRepository(),
          )..add(GetLocationStream());
        }),
      ],
      child: Fixit(
        userRepository: userRepository,
        mapRepository: mapRepository,
//            alertRepository: alertRepository
      ),
    ),
  );
}


class Fixit extends StatelessWidget {
  final UserRepository _userRepository;
  final MapRepository _mapRepository;

  Fixit(
      {Key key,
      @required UserRepository userRepository,
      @required MapRepository mapRepository}) //,
//      @required AlertRepository alertRepository})
      : assert(userRepository != null),
        _userRepository = userRepository,
        _mapRepository = mapRepository,
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocBuilder<AuthenticationBloc, AuthenticationState>(
        builder: (context, state) {
          if (state is Unauthenticated) {
            return LoginScreen(userRepository: _userRepository);
          }
          if (state is Authenticated) {
            BlocProvider.of<MapBloc>(context).add(GetLocationStream());
            return MapScreen(
                mapRepository: _mapRepository, name: state.displayName);
          }
          if (state is Unauthenticated) {
            return LoginScreen(userRepository: _userRepository);
          }
          return SplashScreen();
        },
      ),
    );
  }
} 

MapScreen:
@override Widget build(BuildContext context) { return BlocBuilder<MapBloc, MapState>( bloc: MapBloc(mapRepository: _mapRepository), builder: (BuildContext context, MapState state) { LatLng userLocation = (state as LocationStream).location; return Scaffold( appBar: AppBar(

now I have this prints from console:

Restarted application in 1,976ms.
flutter: Event is AppStarted
flutter: Transaction is Transition { currentState: Uninitialized, event: AppStarted, nextState: Authenticated }
flutter: Event is Instance of 'GetLocationStream'
flutter: Event is Instance of 'GetLocationStream'
flutter: MapBloc event received : Instance of 'GetLocationStream'
flutter: getLocationStream() called
flutter: Erros is NoSuchMethodError: The getter 'latitude' was called on null.
Receiver: null
Tried calling: latitude
flutter: MapBloc event received : Instance of 'GetLocationStream'
flutter: getLocationStream() called
flutter: Erros is NoSuchMethodError: The getter 'latitude' was called on null.
Receiver: null
Tried calling: latitude
flutter: getLocationStream() location is : LatLng(latitude:37.326488, longitude:-122.019769)
flutter: getLocationStream() location is : LatLng(latitude:37.326488, longitude:-122.019769)
flutter: getLocationStream() location is : LatLng(latitude:37.326488, longitude:-122.019769)
flutter: Event is Instance of 'GetLocationStream'
flutter: MapBloc event received : Instance of 'GetLocationStream'
flutter: getLocationStream() called
flutter: Erros is NoSuchMethodError: The getter 'latitude' was called on null.
Receiver: null
Tried calling: latitude
flutter: getLocationStream() location is : LatLng(latitude:37.326454, longitude:-122.019771)
flutter: getLocationStream() location is : LatLng(latitude:37.326423, longitude:-122.019772)
flutter: getLocationStream() location is : LatLng(latitude:37.326391, longitude:-122.019769)
flutter: getLocationStream() location is : LatLng(latitude:37.32636, longitude:-122.019764)

so it actually now triggers MapRepository()'s getLocationStream() method, dough I see a double

flutter: Event is Instance of 'GetLocationStream'

at the beginning of console print .
Thinking that the cause was calling the add() method twice, I commented out BlocProvider.of<MapBloc>(context).add(GetLocationStream()); before returning MapScreen in main() but getLocationStream() doesn't get triggered anymore.
I tied then deleting the add() method from BlocProvider<MapBloc> instead but still getLocationStream() doesn't get triggered.

Also, I see three times the print :

flutter: MapBloc event received : Instance of 'GetLocationStream'
flutter: getLocationStream() called
flutter: Erros is NoSuchMethodError: The getter 'latitude' was called on null.
Receiver: null
Tried calling: latitude

and if I'm not wrong that means that from MapBloc is not returning any value with LocationStream(locationStream) state that inside BlocBuilder<MapBloc, MapState> I assign to userLocation variable with
LatLng userLocation = (state as LocationStream).location; right?
If so I also have errors in that..

Can you see where I get thing twisted?

If you could provide some code correction it would help me very much to relate it to my code and see where I get things twisted.
Thank you so much again.

@felangel Hi Felix, unfortunately I'm not .. but if the code isn't enough to understand the problem
I'll put together a single screen app. Thank you very much.

@vinnytwice it's a lot harder to try to understand the code via snippets like this. It would make it much easier for others to help you if you provide a sample app which people can run locally. If you can't share the exact code, please create a simple example which reproduces the issue. Thanks!

@felangel Hi, sure a sample app is the way to go but there is no deed for it anymore as I actually fixed the code part inherent to bloc, as I changed my _mapGetLocationStreamToState() from

Stream<MapState> _mapGetLocationStreamToState(
      GetLocationStream event) async* {
    print(
        '_mapGetLocationStreamToState latitute : ${_mapRepository.getLocationStream().latitude}');
    print(
        '_mapGetLocationStreamToState longitute : ${_mapRepository.getLocationStream().longitude}');
    locationStream = LatLng(_mapRepository.getLocationStream().latitude,
        _mapRepository.getLocationStream().longitude);
    print('_mapGetLocationStreamToState() locationStream is: $locationStream ');
    yield LocationStream(locationStream);
  }

to

Stream<MapState> _mapGetLocationStreamToState(
      GetLocationStream event) async* {
    print('_mapGetLocationStreamToState event received : $event');

    locationStream = await _mapRepository.getLocationStream();
    print('_mapGetLocationStreamToState() locationStream is: $locationStream ');
    yield LocationStream(locationStream);
  }

and now finally yields a `GetLocationStream()' state. Dough value is still null at least now, the bloc mechanics are in place and working.
I finally see it in console:

Transaction is Transition { currentState: LocationStream {location: null}, event: Instance of 'GetLocationStream', nextState: LocationStream {location: null} }

So now the problem has been narrowed down Repository's getLocationStream() method which returns a null so I'm sure I'm just not returning the value properly.

I also tried changing the return type from LatLng to Future<Latng> :

Future<LatLng> getLocationStream() {
    print('getLocationStream() called');
    print('isTracking was : $isTracking');
    Future<LatLng> location;
    LocationOptions locationOptions = LocationOptions(
        accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 0);
    try {
      if (isTracking == true) {
        _positionStreamSubsciption.cancel();
//        isTracking = !isTracking;
//        print('isTracking was ${!isTracking} and now is : $isTracking');
      } else {
        _positionStreamSubsciption = locationManager
            .getPositionStream(locationOptions)
            .listen((Position position) {
          if (position != null) {
            location =
                LatLng(position.latitude, position.longitude) as Future<LatLng>;
//            return location;

          }
          print('getLocationStream() location is : $location');
//          return location;
        });
//        return location;
      }

      isTracking = !isTracking;
      print('isTracking is : $isTracking');
//      return location;
    } catch (error) {
      print('startTracking error: $error');
    }
  }

but that got me a new type of error :

Unhandled Exception: type 'LatLng' is not a subtype of type 'Future' in type cast

@felangel little update. I almost solved it.
After fiddling a bit with the code I decided to change approach, and seems that I chosen the wrong one before. Instead of returning a LatLng from the repository method, I decided to transform the Stream<Position> coming from GeolocatorAPI directly into a Stream<LatLng>, and listening to it from bloc.

Now states are flowing as expected carrying the new location value with them.

The only problem left to solve(that I actually thought I didn't have) is that MapScren's BlocBuilder doesn't get hold of the value coming from the new state and I get null when using it as in button's callback _mapController.move(userLocation, 16);.

Isn't LatLng userLocation = (state as LocationStream).location;the right way to get hold of it?

bloc: MapBloc(mapRepository: _mapRepository),
        builder: (BuildContext context, MapState state) {
          LatLng userLocation = (state as LocationStream).location;
          return Scaffold(

@vinnytwice It might be because you are creating a new MapBloc instance every time the UI rebuilds.. Try using a BlocProvider instead to get the instance in your BlocBuilder.

@davidmokos how am I creating a new bloc every time? BlocBuilder isn't just listening to a specific bloc and rebuild the returned widget tree ? I already used BlocProvider in main() to be able to send an event to MapBloc and present this screen with data already flowing in. See the beginning of the issue code.

@davidmokos. I think I got it. Commenting out bloc: MapBloc(mapRepository: _mapRepository), now works as expected. So bloc: is actually creating a bloc as BlocProvider would, not just a reference to a specific Bloc. If I'm correct on this, then the correct use of bloc:would be to provide needed (locally and not globally as I need here) bloc to the new rebuilt widget tree without using a BlocProvider.
Please confirm I got this right this time.

@vinnytwice that's correct! You should pretty much never create a new bloc in your BlocBuilder. I would recommend always omitting the bloc property and providing the Bloc type and State type so that BlocBuilder can perform the lookup automatically 馃憤

Closing for now but feel free to comment with additional questions and I'm happy to continue the conversation 馃憤

@felangel Beautiful then I finally got the concept. I misunderstood this in your API Reference docs.
Thank you very much for the library. It is actually making my life easier getting into the new programming paradigm, as I'm porting my app from swift to Flutter and get it ready for production.
Cheers.

@felangel @davidmokos @warriorCoder

Just a little update.. the strage behaviour I was seeing had to do with MapController from flutter_map..
It results that FlutterMap has to be in a stateful widget and instantiated in the widget state..else will sort of loose connection with FlutterMap..
So after changing MapScreen to a stateful widget I had to swap BlocBuilder for a BlocListener and call setState in its callback. That fixed it. No more The method 'move' was called on null.error and Hot-reload/restart don't cause any trouble anymore. Still FlutterMap doesn't get drawn on the incoming state coordinates..

Hey @vinnytwice 馃憢
I've opened a PR with some suggestions around your bloc usage which seem to resolve the issues you're having.

Hope that helps 馃憤

Was this page helpful?
0 / 5 - 0 ratings