Skip to content

initialCameraFit can silently apply against a zero-sized parent and never retry with the correct size #2222

Description

@nisenbeck

What is the bug?

initialCameraFit is meant to fit the camera to a given LatLngBounds once, when the map first appears. If the map is nested under a parent that doesn't have its final size on the very first layout pass (e.g. an Expanded/Flexible inside a not-yet-measured parent, an AnimatedSize, a lazily-revealed tab, ...), the fit can be computed against a Size(0, 0) camera instead of the real, final size. The result is a degenerate (fully zoomed-out, whole-world) camera position - and because the "initial fit applied" flag is set regardless, it is never retried once the real size becomes available a moment later. The map keeps showing the wrong view for as long as that FlutterMap widget stays mounted, until the user manually pans or zooms (the camera itself isn't frozen - only the automatic one-time fit never re-runs).

This doesn't crash the app, but it silently defeats the one thing initialCameraFit is meant to do, with no error or log to point at the cause - I'd classify this as a moderate-severity, silent UX bug rather than critical.

Root cause

_FlutterMapStateContainer._parentConstraintsAreSet (lib/src/map/widget.dart):

/// During Flutter startup the native platform resolution is not immediately
/// available which can cause constraints to be zero before they are updated
/// in a subsequent build to the actual constraints. This check allows us to
/// differentiate zero constraints caused by missing platform resolution vs
/// zero constraints which were actually provided by the parent widget.
bool _parentConstraintsAreSet(
        BuildContext context, BoxConstraints constraints) =>
    constraints.maxWidth != 0 || MediaQuery.sizeOf(context) != Size.zero;

This is meant to distinguish "constraints are zero because the platform hasn't resolved yet" from "constraints are zero because the parent widget actually gave zero space". In practice, MediaQuery.sizeOf(context) is almost always already non-zero (the device's screen size is known essentially immediately), even while constraints.maxWidth is still 0 for one frame because this specific widget's layout hasn't settled yet - the two are independent signals, and the || makes the check pass in exactly the case it's supposed to catch.

_applyInitialCameraFit then runs with constraints still zero-sized:

void _applyInitialCameraFit(BoxConstraints constraints) {
  if (!_initialCameraFitApplied &&
      widget.options.initialCameraFit != null &&
      _parentConstraintsAreSet(context, constraints)) {
    _initialCameraFitApplied = true;

    _mapController.fitCamera(widget.options.initialCameraFit!);
  }
}

_initialCameraFitApplied is set to true unconditionally once this runs, so even though _updateAndEmitSizeIfConstraintsChanged will be called again with the real, non-zero size a moment later, the fit is never recomputed.

How can we reproduce it?

A minimal reproduction is available at https://github.com/nisenbeck/flutter_map_mre, branch repro/zero-size-initial-fit:

git clone https://github.com/nisenbeck/flutter_map_mre
cd flutter_map_mre
git checkout repro/zero-size-initial-fit
flutter pub get
flutter run

It wraps FlutterMap in a small widget that reports Size.zero for exactly one frame before resizing to fill the screen - mimicking the real-world layout patterns mentioned above.

  • Expected: the map opens framed on the configured bounds (roughly Germany/Switzerland/Austria).
  • Actual (when the bug reproduces): the map opens zoomed all the way out to the whole world, and stays that way until manual interaction.

This is timing-sensitive - if it doesn't reproduce on the very first launch, a few hot restarts (R in the flutter run terminal) reliably trigger it.

I also have a deterministic widget test (no device/timing dependency) demonstrating the same thing directly against FlutterMap, which I'll include with the fix PR.

Do you have a potential solution?

Fixed in #2223.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    Medium

    Effort

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions