Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crash when calling setState on onPositionChanged #374

Closed
spvalencia opened this issue Jul 26, 2019 · 12 comments
Closed

Crash when calling setState on onPositionChanged #374

spvalencia opened this issue Jul 26, 2019 · 12 comments
Labels
bug This issue reports broken functionality or another error

Comments

@spvalencia
Copy link

spvalencia commented Jul 26, 2019

I want to display the map position (latitude and longitude) when the position changes (onPositionChanged).

When I call setState to do that the app crash.

Screen Shot 2019-07-26 at 3 43 09 PM

onPositionChanged: (option, gestures, _) {
setState(() => _mapLocation = option.center);
},

@johnpryan
Copy link
Collaborator

can you provide a short example?

@spvalencia
Copy link
Author

I fixed adding a little delay.

It occurs when I call setState when map position (center) changes

onPositionChanged: (option, gestures, _) { setState((){}); },

@besedin
Copy link

besedin commented Aug 20, 2019

What about using addPostFrameCallback?

For example,

@override
Widget build(BuildContext context) {
  bool _building = true;

  WidgetsBinding.instance.addPostFrameCallback((_) {
    _building = false;
  });

...

onPositionChanged: (MapPosition position, bool hasGesture, bool isUserGesture) {
  if (!_building)
    setState(() { ... }
}

@johnpryan johnpryan added bug This issue reports broken functionality or another error and removed question labels Aug 20, 2019
@johnpryan
Copy link
Collaborator

It seems like the onPositionChanged callback is getting invoked during a build()

@nicowernli
Copy link

Same happens to me. The onPositionChanged is getting invoked with the initial map positioning I guess.

@alves-luis
Copy link

What about using addPostFrameCallback?

For example,

@override
Widget build(BuildContext context) {
  bool _building = true;

  WidgetsBinding.instance.addPostFrameCallback((_) {
    _building = false;
  });

...

onPositionChanged: (MapPosition position, bool hasGesture, bool isUserGesture) {
  if (!_building)
    setState(() { ... }
}

This solved the issue for me.

@maRci002
Copy link
Contributor

#719 - MapController has mapEventStream which can be listened any time and it won't cause any problem since events are
async.

  @override
  void initState() {
    super.initState();
    mapController = MapController();

    subscription = mapController.mapEventStream.listen(onMapEvent);
  }

  void onMapEvent(MapEvent mapEvent) {
    if (mapEvent is MapEventMoveStart) {
      // do something
    } else if (mapEvent is MapEventMove) {
      // do something
    } else if (mapEvent is MapEventMoveEnd) {
      // do something
    }
  }

@barbalex
Copy link

barbalex commented Jun 4, 2021

This happened to me to as inside onPositionChanged I had code updating state for the center coordinates of the map because the map presents them.

Seems to me this could be a rather frequent use case. And being a dart/flutter noob it took me quite some time to drill down to the reason. I feel that this is a real issue that will hit a lot of devs. There should be a better solution than closing it.

The problem is the setState call. As onPositionChanged gets called before the widget has finished building, it causes the crash.

I propose two solutions:

  • For the knowing the easiest way is to declare the onPositionChanged callback async.
    This is simpler than using addPostFrameCallback.
  • Create a new method that is not called during the build and use the two method's explanations that appear when hovering over the methods in a modern editor to tell the user this important difference.
    I am not sure how to call this method but one idea that serves mostly making my point clearer would be onPositionChangedAfterInitialBuild.

Here my code:

onPositionChanged: (MapPosition position, bool hasGesture) async {
  double? newLat = position.center?.latitude;
  double? newLng = position.center?.longitude;
  if (newLat != null && newLng != null) {
    setState(() {
      lat = newLat;
      lng = newLng;
    });
  }
},

@ibrierley
Copy link
Collaborator

Being an idiot here...what's the reasoning for calling setState during an onPositionChanged ? Just trying to understand if there's a separate issue that needs pondering.

I'm all for helping evade the issue with async or whatever, but just checking that there isn't something else amiss, even if it's documentation or whatever.

@barbalex
Copy link

barbalex commented Jun 4, 2021

I have this widget inside nonRotatedChildren:

Padding(
  padding: const EdgeInsets.only(top: 33, left: 10),
  child: Align(
    child: Text(
      'Lat: ${lat.toPrecision(4)}, Lng: ${lng.toPrecision(4)}',
      style: TextStyle(
          fontSize: 10,
          fontWeight: FontWeight.bold,
          color: Theme.of(context).primaryColor),
    ),
    alignment: Alignment.topLeft,
  ),
),

It displays the current coordinates of the map centre:
InkedScreenshot_1622801648_LI

This is what lat and long fetched inside onPositionChanged are used for:

onPositionChanged: (MapPosition position, bool hasGesture) async {
  double? newLat = position.center?.latitude;
  double? newLng = position.center?.longitude;
  if (newLat != null && newLng != null) {
    setState(() {
      lat = newLat;
      lng = newLng;
    });
  }
},

Maybe there is a much easier way to access the map's center coordinates?

@ibrierley
Copy link
Collaborator

ibrierley commented Jun 4, 2021

Ok, makes sense, the nonRotatedWidgets wouldn't get updated (wondering if something could be improved there somehow).
It feels a bit like that text widget should be a separate entity that gets refreshed. Eg you could do

ValueNotifier<String> latLngString = ValueNotifier<String>("");
...
onPositionChanged: (position, _) {
                if (position.center != null) {
                  latLngString.value = "${position.center?.latitude.toStringAsPrecision(4)}, ${position.center?.longitude.toStringAsPrecision(4)} ";
                }
              }
...
    ValueListenableBuilder(
      valueListenable: latLngString,
        builder: (context, value, widget) {
          return Padding(
            padding: const EdgeInsets.only(top: 33, left: 10),
            child: Align(
              child: Text(
                "Lat: ${latLngString.value}",
                style: TextStyle(
                    fontSize: 10,
                    fontWeight: FontWeight.bold,
                    color: Theme
                        .of(context)
                        .primaryColor),
              ),
              alignment: Alignment.topLeft,
            ),
          );
        }
    )

Feels a bit clunky though and not ideal, so I can see why people just call setState (I've done similar myself in some plugins in the past and should probably rewrite)

@barbalex
Copy link

barbalex commented Jun 4, 2021

Yeah, or use getx's .obs to create an observable value (https://pub.dev/packages/get#reactive-state-manager).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug This issue reports broken functionality or another error
Projects
None yet
Development

No branches or pull requests

8 participants