mapbox / mapbox/mapbox-maps-flutter

[iOS] LineLayer silently dropped from style when lineDasharray is set

Open
#1,121 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Dart
Stars
380
Forks
204
PR merge metrics
No merged PRs in 30d

Description

## Summary

On iOS, setting `LineLayer.lineDasharray` causes the entire layer to be silently dropped from the rendered style. The Dart-side `addLayer` call returns success, but the layer never persists in the style. Visual: the dashed line never renders. Programmatic: subsequent calls to `setStyleLayerProperty` against that layer id throw `PlatformException(0, "Layer ... is not in style")`.

**Android renders the same `LineLayer` correctly with `lineDasharray` set, on the same `mapbox_maps_flutter` versions.**

This bug has persisted across at least 9 months of releases and **its symptom mutated between versions**, which is what makes it hard to defend against in user code:

| Version | Symptom |
|---|---|
| 2.12.0 | `addLayer` returns ok → `style.getLayer(id)` returns `null` **immediately** → first `setStyleLayerProperty` call throws `Layer ... is not in style` |
| 2.21.1 | `addLayer` returns ok → `style.getLayer(id)` returns the layer **at the immediate audit** → layer **disappears from the style a few milliseconds later** → next `setStyleLayerProperty` call returns `null` from `getLayer` |

In other words: on the current version, you cannot detect the bug by checking `getLayer` immediately after `addLayer`. The check passes. The layer is gone by the time anything tries to interact with it.

## Affected versions

- `mapbox_maps_flutter` **2.12.0** — confirmed broken (Sep 2024 era)
- `mapbox_maps_flutter` **2.21.1** — confirmed broken with mutated symptom (current latest at time of filing)

Tested on:
- iPhone 17, iOS 26.3.1, Xcode 26.3
- Compared against: moto g play 2024, Android 14 (API 34) — works correctly on both versions

Flutter:
- Original: 3.24.5 / Dart 3.5.4
- Retest: 3.41.6

## Minimal reproduction

```dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';

void main() {
MapboxOptions.setAccessToken(const String.fromEnvironment('MAPBOX_TOKEN'));
runApp(const MaterialApp(home: BugRepro()));
}

class BugRepro extends StatefulWidget {
const BugRepro({super.key});
@override
State createState() => _BugReproState();
}

class _BugReproState extends State {
static const _sourceId = 'repro-source';
static const _layerId = 'repro-line-layer';

@override
Widget build(BuildContext context) => Scaffold(
body: MapWidget(
styleUri: MapboxStyles.SATELLITE_STREETS,
cameraOptions: CameraOptions(
center: Point(coordinates: Position(-122.4885, 37.6244)),
zoom: 15,
),
onStyleLoadedListener: (_) => _onStyleLoaded(),
onMapCreated: (map) => _map = map,
),
);

MapboxMap? _map;

Future _onStyleLoaded() async {
final map = _map;
if (map == null) return;

// A simple LineString feature on the map.
final geojson = jsonEncode({
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'LineString',
'coordinates': [
[-122.4895, 37.6249],
[-122.4883, 37.6220],
],
},
}
],
});

await map.style.addSource(GeoJsonSource(id: _sourceId, data: geojson));

// The bug: setting lineDasharray causes the LineLayer to silently
// disappear on iOS. Comment out the lineDasharray line below and the
// line renders correctly (as a solid line).
await map.style.addLayer(LineLayer(
id: _layerId,
sourceId: _sourceId,
lineColor: 0xFFFFFFFF,
lineWidth: 3.0,
lineDasharray: [4.0, 3.0], // ← REMOVE THIS LINE → bug goes away
));

// Diagnostic: ask the style whether the layer actually landed.
// - Without lineDasharray: prints `layer present? true` and the
// line renders.
// - With lineDasharray on iOS: prints `layer present? true` here
// (on 2.21.1; on 2.12.0 it prints `false`), but the line does
// NOT render visually, and a few milliseconds later getLayer
// starts returning null.
final immediate = await map.style.getLayer(_layerId);
debugPrint('layer present immediately? ${immediate != null}');

// Re-check after a short delay — this is the 2.21.1 mutated symptom.
await Future.delayed(const Duration(milliseconds: 100));
final delayed = await map.style.getLayer(_layerId);
debugPrint('layer present after 100ms delay? ${delayed != null}');
}
}
```

Run with:

```
flutter run -d --dart-define=MAPBOX_TOKEN=pk.xxx
```

## Expected behavior

The `LineLayer` is added to the style, the dashed line renders on the map, and subsequent `getLayer(id)` calls return the layer. This is what happens on Android with the exact same code, and on iOS when `lineDasharray` is omitted.

## Actual behavior

On iOS, with `lineDasharray` set:

- The dashed line never renders visually.
- On 2.12.0: the `layer present immediately?` debug print outputs `false`. Subsequent `setStyleLayerProperty` calls throw `PlatformException(0, "Layer ... is not in style")`.
- On 2.21.1: the `layer present immediately?` print outputs `true`, but `layer present after 100ms delay?` prints `false`. The layer was added, briefly visible to `getLayer`, then disappeared from the style without any error or callback.

## Workaround

Don't set `lineDasharray` cross-platform. Either accept solid lines on both platforms (simplest), or render dashes via a series of pre-spaced LineString features in the GeoJSON source (medium scope, no upstream dependency).

For our use case (a golf course mapping app rendering hole-line indicators), we accepted solid lines as the cross-platform default and documented the constraint as a code-level convention for the team:
https://github.com/oshelot/Caddie-AI/blob/kan-251-mobile-flutter-scaffold/mobile-flutter/docs/CONVENTIONS.md#2-style-layer-adds--always-use-tryaddlayer--verifylayerspresent-and-avoid-known-broken-properties

## Why the symptom mutation matters

The diagnostic pattern that catches Bug-2-class failures in user code on 2.12.0 (audit `getLayer` immediately after `addLayer`) does not work on 2.21.1. Whatever change was made in the intervening 9 months made the failure window asynchronous — the layer is committed long enough to pass an immediate audit, then dropped before the first style mutation. User code that relied on the audit pattern (which several open issues in this repo recommend, including #568) is now silently broken.

Filing this with the symptom mutation made explicit so a fix can address both observable behaviors, not just the one visible at the moment of `addLayer`.

## Related

Possibly related to but not duplicates of:
- #568 — Adding GeoJSON Layer in onMapCreated is unreliable (similar timing-sensitive area, but about onMapCreated lifecycle, not specific properties)
- #527 — `getLayer("puck") throws "Layer puck is not in style"` (same exception class, different cause)

## Spike report context

This issue was filed during a Flutter migration spike for an existing dual-native (Swift + Kotlin) app. The spike compared `mapbox_maps_flutter` against the native iOS and Android Mapbox SDKs and found that this bug + #issue-for-bug-3 are the only blockers preventing 1:1 visual parity with the native iOS app. Full report (including measured frame times, layer-render latency, and side-by-side device screenshots): https://github.com/oshelot/Caddie-AI/blob/kan-252-flutter-spike/flutter-spike/SPIKE_REPORT.md

Happy to provide additional logs, a smaller repro project, or test on additional iOS versions if helpful.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the minimal Flutter reproduction in the issue and run it with `flutter run -d --dart-define=MAPBOX_TOKEN=pk.xxx`. Compare `LineLayer` behavior with and without `lineDasharray`, including `getLayer` immediately and after 100ms, and compare against Android. Done means the dashed layer remains in the iOS style, renders, and continues to be returned by `getLayer`.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart, flutter, ios
Domain
mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.