tlserver / flutter_map_location_marker

A flutter map plugin for displaying device current location.
https://pub.dev/packages/flutter_map_location_marker
BSD 3-Clause "New" or "Revised" License
97 stars 82 forks source link

The position of `CurrentLocationLayer` will cause errors #97

Closed jiazeh closed 6 months ago

jiazeh commented 6 months ago

Describe the bug When the CurrentLocationLayer is overlaid on the dynamic layer, an error of Unhandled Exception: Bad state: Stream has not been Listened. will be thrown.

Error Message

E/flutter (21847): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Bad state: Stream has already been listened to.
E/flutter (21847): #0      _StreamController._subscribe (dart:async/stream_controller.dart:686:7)
E/flutter (21847): #1      _ControllerStream._createSubscription (dart:async/stream_controller.dart:836:19)
E/flutter (21847): #2      _StreamImpl.listen (dart:async/stream_impl.dart:471:9)
E/flutter (21847): #3      _CurrentLocationLayerState._subscriptFollowCurrentLocationStream (package:flutter_map_location_marker/src/widgets/current_location_layer.dart:485:37)
E/flutter (21847): #4      _CurrentLocationLayerState._subscriptPositionStream.<anonymous closure> (package:flutter_map_location_marker/src/widgets/current_location_layer.dart:401:13)
E/flutter (21847): #5      _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10)
E/flutter (21847): #6      _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
E/flutter (21847): #7      _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
E/flutter (21847): #8      _ForwardingStreamSubscription._add (dart:async/stream_pipe.dart:123:11)
E/flutter (21847): #9      _MapStream._handleData (dart:async/stream_pipe.dart:218:10)
E/flutter (21847): #10     _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10)
E/flutter (21847): #11     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
E/flutter (21847): #12     _DelayedData.perform (dart:async/stream_impl.dart:515:14)
E/flutter (21847): #13     _PendingEvents.handleNext (dart:async/stream_impl.dart:620:11)
E/flutter (21847): #14     _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:591:7)
E/flutter (21847): #15     _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
E/flutter (21847): #16     _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)

To Reproduce

class _CenterFabExampleState extends State<CenterFabExample> {
  late AlignOnUpdate _alignPositionOnUpdate;
  late final StreamController<double?> _alignPositionStreamController;
  bool _enableLocService = false;
  bool _enableMapRoadmap = false;

  @override
  void initState() {
    _alignPositionOnUpdate = AlignOnUpdate.always;
    _alignPositionStreamController = StreamController<double?>();
    super.initState();
  }

  @override
  void dispose() {
    _alignPositionStreamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Center FAB Example'),
      ),
      body: FlutterMap(
        options: MapOptions(
          initialCenter: const LatLng(0, 0),
          initialZoom: 1,
          minZoom: 0,
          maxZoom: 19,
          onPositionChanged: (MapPosition position, bool hasGesture) {
            if (hasGesture && _alignPositionOnUpdate != AlignOnUpdate.never) {
              setState(
                () => _alignPositionOnUpdate = AlignOnUpdate.never,
              );
            }
          },
        ),
        children: [
          TileLayer(
            urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
            userAgentPackageName:
                'net.tlserver6y.flutter_map_location_marker.example',
            maxZoom: 19,
          ),
          // The layer can be displayed normally if placed here, but it will be obscured.
          if (_enableLocService)
            CurrentLocationLayer(
              alignPositionStream: _alignPositionStreamController.stream,
              alignPositionOnUpdate: _alignPositionOnUpdate,
            ),
          if (_enableMapRoadmap)
            TileLayer(
              urlTemplate:
                  'https://s3.amazonaws.com/te512.safecast.org/{z}/{x}/{y}.png',
              maxZoom: 16,
            ),
          // An error will occur if the layer is placed here.
          // if (_enableLocService)
          //   CurrentLocationLayer(
          //     alignPositionStream: _alignPositionStreamController.stream,
          //     alignPositionOnUpdate: _alignPositionOnUpdate,
          //   ),
        ],
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'toggle',
            onPressed: () => setState(() {
              _enableMapRoadmap = !_enableMapRoadmap;
            }),
            child: const Icon(Icons.legend_toggle),
          ),
          const SizedBox(height: 12),
          FloatingActionButton(
            heroTag: 'location',
            onPressed: () {
              setState(() {
                _enableLocService = true;
                _alignPositionOnUpdate = AlignOnUpdate.always;
              });
              _alignPositionStreamController.add(14);
            },
            child: const Icon(
              Icons.my_location,
              color: Colors.white,
            ),
          ),
        ],
      ),
    );
  }
}

Trigger wrong behavior After location, when the following code is clicked.

FloatingActionButton(
  heroTag: 'toggle',
  onPressed: () => setState(() {
    _enableMapRoadmap = !_enableMapRoadmap;
  }),
  child: const Icon(Icons.legend_toggle),
),

Screenshots work_fine work_error

Smartphone Platforms:

Additional context This problem only occurs in the following package versions: 8.0.0, 8.0.1, 8.0.2, 8.0.3

tlserver commented 6 months ago

Cannot repoduce. image I think you are facing this error when doing hot reload. The error message show that you subscript the stream twice. A non-boardcast stream can only be subscript once. You should recreate the stream each time if you unsubscript. If you remove the CurrentLocationLayer by hot reload, your _alignPositionStreamController.stream is already disposed. It cannot be used again. If you really need to reuse the stream, consider use a boardcast stream.

StreamController.broadcast();
jiazeh commented 6 months ago

Thank you so much for your reply. Here is a record of how the error occurred and the situation after the problem was solved... _P.S. The symbol "!!!" is represents the number of calls to _subscriptFollowCurrentLocationStream._

Cause of the problem

https://github.com/tlserver/flutter_map_location_marker/assets/45085050/c6dd08cf-dde5-44d7-a5c3-50c959169366

The result after switching to the StreamController.broadcast()

https://github.com/tlserver/flutter_map_location_marker/assets/45085050/805bc15a-3f87-4af4-8aad-bb16a9c3b87e

Code to reproduce

class _CenterFabExampleState extends State<CenterFabExample> {
  late AlignOnUpdate _alignPositionOnUpdate;
  late final StreamController<double?> _oldPositionStreamController;
  // late final StreamController<double?> _newPositionStreamController;
  bool _enableLocService = false;
  bool _enableMapRoadmap = false;

  @override
  void initState() {
    _alignPositionOnUpdate = AlignOnUpdate.always;
    _oldPositionStreamController = StreamController<double?>();
    // _newPositionStreamController = StreamController<double?>.broadcast();
    super.initState();
  }

  @override
  void dispose() {
    _oldPositionStreamController.close();
    // _newPositionStreamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Center FAB Example'),
      ),
      body: FlutterMap(
        options: MapOptions(
          initialCenter: const LatLng(0, 0),
          initialZoom: 1,
          minZoom: 0,
          maxZoom: 19,
          onPositionChanged: (MapPosition position, bool hasGesture) {
            if (hasGesture && _alignPositionOnUpdate != AlignOnUpdate.never) {
              setState(
                    () => _alignPositionOnUpdate = AlignOnUpdate.never,
              );
            }
          },
        ),
        children: [
          TileLayer(
            urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
            userAgentPackageName:
            'net.tlserver6y.flutter_map_location_marker.example',
            maxZoom: 19,
          ),
          // The layer can be displayed normally if placed here, but it will be obscured.
          // if (_enableLocService)
          //   CurrentLocationLayer(
          //     alignPositionStream: _oldPositionStreamController.stream,
          //     // alignPositionStream: _newPositionStreamController.stream,
          //     alignPositionOnUpdate: _alignPositionOnUpdate,
          //   ),
          if (_enableMapRoadmap)
            TileLayer(
              urlTemplate:
              'https://s3.amazonaws.com/te512.safecast.org/{z}/{x}/{y}.png',
              maxZoom: 16,
            ),
          // An error will occur if the layer is placed here.
          if (_enableLocService)
            CurrentLocationLayer(
              alignPositionStream: _oldPositionStreamController.stream,
              // alignPositionStream: _newPositionStreamController.stream,
              alignPositionOnUpdate: _alignPositionOnUpdate,
            ),
        ],
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'toggle',
            onPressed: () => setState(() {
              _enableMapRoadmap = !_enableMapRoadmap;
            }),
            child: const Icon(Icons.legend_toggle),
          ),
          const SizedBox(height: 12),
          FloatingActionButton(
            heroTag: 'location',
            onPressed: () {
              setState(() {
                _enableLocService = true;
                _alignPositionOnUpdate = AlignOnUpdate.always;
              });
              _oldPositionStreamController.add(14);
              // _newPositionStreamController.add(14);
            },
            child: const Icon(
              Icons.my_location,
              color: Colors.white,
            ),
          ),
        ],
      ),
    );
  }
}