mikes222 / mapsforge_flutter

Offline maps as pure flutter code
GNU Lesser General Public License v3.0
50 stars 19 forks source link

A JobSet was used after being disposed. #107

Closed vasilevzhivko closed 3 weeks ago

vasilevzhivko commented 4 months ago

I am trying to implement a log in/log out functionality using Provider for controlling the map. Everything runs if I execute the straight flow - log in and use the map. The problem comes when I log out and try to move the map around, then I get this exception:

The following assertion was thrown building _LayerPainter(state: _LayerState#912db):
A JobSet was used after being disposed.
Once you have called dispose() on a JobSet, it can no longer be used.

The relevant error-causing widget was:
  _LayerPainter
  _LayerPainter:file:///Users/zhivkovasilev/.pub-cache/git/mapsforge_flutter-fd413b440980d3755f671511400f708fe8e1b34f/mapsforge_flutter/lib/src/vie
  w/mapview_widget.dart:229:9

When the exception was thrown, this was the stack:
#0      ChangeNotifier.debugAssertNotDisposed.<anonymous closure> (package:flutter/src/foundation/change_notifier.dart:179:9)
#1      ChangeNotifier.debugAssertNotDisposed (package:flutter/src/foundation/change_notifier.dart:186:6)
#2      ChangeNotifier.addListener (package:flutter/src/foundation/change_notifier.dart:271:27)
#3      CustomPainter.addListener (package:flutter/src/rendering/custom_paint.dart:160:56)
#4      RenderCustomPaint.attach (package:flutter/src/rendering/custom_paint.dart:544:25)
#5      RenderObject.adoptChild (package:flutter/src/rendering/object.dart:1841:13)
#6      ContainerRenderObjectMixin.insert (package:flutter/src/rendering/object.dart:4255:5)
#7      MultiChildRenderObjectElement.insertRenderObjectChild (package:flutter/src/widgets/framework.dart:6843:18)
#8      RenderObjectElement.attachRenderObject (package:flutter/src/widgets/framework.dart:6602:35)
#9      RenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6467:5)
#10     SingleChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6768:11)
#11     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4340:16)
#12     Element.updateChild (package:flutter/src/widgets/framework.dart:3843:20)
#13     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5512:16)
#14     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5650:11)
#15     Element.rebuild (package:flutter/src/widgets/framework.dart:5203:7)
#16     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2905:19)
#17     WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:1136:21)
#18     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:443:5)
#19     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1392:15)
#20     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1313:9)
#21     SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1171:5)
#22     _invoke (dart:ui/hooks.dart:312:13)
#23     PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:419:5)
#24     _drawFrame (dart:ui/hooks.dart:283:31)

════════════════════════════════════════════════════════════════════════════════════════════════════

this is my provider code:


class MapViewModel with ChangeNotifier {
  late MapDataStore map;
  late JobRenderer jobRenderer;
  late SymbolCache symbolCache = FileSymbolCache();
  late FileTileBitmapCache bitmapCache;
  late DisplayModel displayModel =
      DisplayModel(fontScaleFactor: 0.5, maxZoomLevel: 19);
  late ViewModel viewModel;
  late MapModel mapModel;
  bool initialLocationSet = false;
  bool _disposed = false; // Flag to track disposal state
  MarkerByItemDataStore currentLocationMarkerStore = MarkerByItemDataStore();
  MarkerByItemDataStore hutsMarkerstore = MarkerByItemDataStore();
  GpsService gpsService = GpsService();
  List<Marker> tappedMarker = [];
  StreamSubscription<geo.Position>? _gpsSubscription;
  Function(int)? onMarkerTappedCallback;

  Future<void> initializeMap() async {
    _resetState();
    try {
      bool permissionsGranted = await gpsService.checkPermissionsAndServices();
      if (!permissionsGranted) {
        throw Exception('Location permissions not granted');
      }
      await createMapModel();
      await createViewModel();

      gpsService.startPositioning();
      // Start listening to GPS updates
      _startGpsListening();

      notifyListeners(); // Notify listeners that everything is initialized
    } catch (e) {
      if (!_disposed) {
        print('Failed to initialize map: $e');
        throw Exception('Failed to initialize MapViewModel');
      }
    }
  }

  void _resetState() {
    initialLocationSet = false;
    _gpsSubscription?.cancel();
    currentLocationMarkerStore = MarkerByItemDataStore(); // Recreate the store
    hutsMarkerstore = MarkerByItemDataStore(); // Recreate the store
    tappedMarker = [];
  }

  Future<MapModel> getMapModel() async {
    await Future.delayed(const Duration(milliseconds: 500));
    return mapModel;
  }

  Future<ViewModel> getViewModel() async {
    await Future.delayed(const Duration(milliseconds: 500));
    return viewModel;
  }

  Future<MapModel> createMapModel() async {
    try {
      ByteData content =
          await rootBundle.load('assets/map/mapstyles/bgmountains.map');
      map = await MapFile.using(content.buffer.asUint8List(), null, null);

      // Initialize render theme and renderer
      final renderTheme = await RenderThemeBuilder.create(
        displayModel,
        'assets/map/mapstyles/Elevate.xml',
      );

      jobRenderer = MapDataStoreRenderer(map, renderTheme, symbolCache, true);

      bitmapCache =
          await FileTileBitmapCache.create(jobRenderer.getRenderKey());
      bitmapCache.purgeAll();

      mapModel = MapModel(
        displayModel: displayModel,
        renderer: jobRenderer,
        tileBitmapCache: bitmapCache,
        symbolCache: symbolCache,
      );

      mapModel.markerDataStores.add(currentLocationMarkerStore);
      mapModel.markerDataStores.add(hutsMarkerstore);

      return mapModel;
    } catch (e) {
      if (!_disposed) {
        print('Failed to initialize map: $e');
        throw Exception('Failed to initialize MapViewModel');
      }
      return Future.error('Failed to initialize MapViewModel');
    }
  }

  Future<ViewModel> createViewModel() async {
    viewModel = ViewModel(
      displayModel: displayModel,
      contextMenuBuilder: null, // Remove default ContextMenuBuilder
    );

    viewModel.observeTap.listen((event) {
      if (!_disposed) {
        tappedMarker = hutsMarkerstore.isTapped(event);
        if (tappedMarker.isNotEmpty && tappedMarker[0] is PoiMarker<int>) {
          int hutId = (tappedMarker[0] as PoiMarker<int>).item!;
          onMarkerTappedCallback?.call(hutId); // Call the callback if it's set
        }
      }
    });

    viewModel.setZoomLevel(18);

    return viewModel;
  }

  void _startGpsListening() {
    _gpsSubscription = GpsService.positionStream.listen((position) {
      print(
          "GPS position received: ${position.latitude}, ${position.longitude}");
      if (!initialLocationSet) {
        viewModel.setMapViewPosition(position.latitude, position.longitude);
      }
      _updatePositionMarker(Pos(position.latitude, position.longitude));
    });
  }

  Future<void> _updatePositionMarker(Pos position) async {
    if (!_disposed) {
      currentLocationMarkerStore.clearMarkers();

      final positionMarker = PoiMarker(
        displayModel: displayModel,
        src: 'assets/images/walk-2.png',
        height: 32,
        width: 32,
        latLong: LatLong(position.latitude, position.longitude),
        position: Position.CENTER,
      );

      await positionMarker.initResources(symbolCache);
      currentLocationMarkerStore.addMarker(positionMarker);
      currentLocationMarkerStore.setRepaint();

      notifyListeners(); // Notify listeners that the marker has been updated
    }
  }

  @override
  void dispose() {
    _disposed = true; // Set the disposed flag
    _gpsSubscription?.cancel();
    print('GPS subscription canceled'); // Debugging statement
    currentLocationMarkerStore.dispose();
    hutsMarkerstore.dispose();
    mapModel.dispose();
    super.dispose();
  }
}

and this is where I init the map:

class AuthWrapper extends StatelessWidget {
  const AuthWrapper({Key? key}) : super(key: key);

  Future<bool> _checkAuthState() async {
    final user = FirebaseAuth.instance.currentUser;
    final accessToken = await FacebookAuth.instance.accessToken;
    return user != null || accessToken != null;
  }

  @override
  Widget build(BuildContext context) {
    return Consumer<AuthService>(
      builder: (context, authService, child) {
        return FutureBuilder<bool>(
          future: _checkAuthState(),
          builder: (context, authSnapshot) {
            if (authSnapshot.connectionState == ConnectionState.waiting) {
              return const LoadingView();
            }
            if (authSnapshot.hasError ||
                !authSnapshot.hasData ||
                !authSnapshot.data!) {
              return const LoginScreen();
            }

            return FutureBuilder(
              future: Provider.of<MapViewModel>(context, listen: false)
                  .initializeMap(),
              builder: (context, mapSnapshot) {
                if (mapSnapshot.connectionState != ConnectionState.done) {
                  return const LoadingView(); // Show loading until the initialization is done
                }
                if (mapSnapshot.hasError) {
                  return Center(
                      child:
                          Text('Error initializing map: ${mapSnapshot.error}'));
                }
                return const MainScaffold(body: MapPage());
              },
            );
          },
        );
      },
    );
  }
}
vasilevzhivko commented 1 month ago

hey @mikes222 I was wondering if you are still contributing to the repo?