flutter_background_geolocation icon indicating copy to clipboard operation
flutter_background_geolocation copied to clipboard

MissingPluginException when app turn on again

Open Merynek opened this issue 1 month ago • 16 comments

Required Reading

  • [x] Confirmed

Plugin Version

4.18.1

Mobile operating-system(s)

  • [x] iOS
  • [x] Android

Device Manufacturer(s) and Model(s)

Galaxy s20

Device operating-systems(s)

Android 13

What do you require assistance about?

Hi, I have problem with rerun my geofencing.

  1. Start app (run geofencing,.. -> it works)
  2. Kill app (wait for notification from OS)
  3. Notification Click (run app) Everything ok but I saw error in log: MissingPluginException: MissingPluginException(No implementation found for method listen on channel com.transistorsoft/flutter_background_geolocation/events/geofence)
  4. Next geofence places not fired.

[Optional] Plugin Code and/or Config

// In my GeofenceService:
GeofenceService(this.ref) {
    MeryUtils.logMery('[GeofenceService] CONSTRUKTOR');
    bg.BackgroundGeolocation.ready(bg.Config(
        enableHeadless: true,
        desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
        persistMode: bg.Config.PERSIST_MODE_GEOFENCE,
        notification: bg.Notification(
          title: "",
          text: "",
          priority: bg.Config.NOTIFICATION_PRIORITY_MIN,
          sticky: false,
        ),
        distanceFilter: 10.0,
        stopOnTerminate: false,
        autoSync: true,
        foregroundService: true,
        geofenceModeHighAccuracy: true,
        startOnBoot: true,
        debug: false,
        geofenceInitialTriggerEntry: true,
        logLevel: bg.Config.LOG_LEVEL_DEBUG,
    )).then((bg.State state) async {
      bg.BackgroundGeolocation.onGeofence(handleGeofenceEvent);
      if (!state.enabled) {
        await MeryUtils.logMery('[GeofenceService] READY');
        await bg.BackgroundGeolocation.start();
        await MeryUtils.logMery('[GeofenceService] startGeofences');
        _listenChanges();
      }
    });
  }

final geofenceServiceProvider = Provider.autoDispose<GeofenceService?>((ref) {
  final loggedInAsyncValue = ref.watch(loggedInProvider);
  if (loggedInAsyncValue.isLoading || loggedInAsyncValue.hasError) {
    return null;
  }
  final isLoggedIn = loggedInAsyncValue.value ?? false;
  if (isLoggedIn) {
    return GeofenceService(ref);
  }
  return null;
});


// Alive
class AlwaysAliveNotifier extends AsyncNotifier<void> {
  @override
  Future<void> build() async {
    ref.watch(geofenceServiceProvider.select((_) => true));
  }
}

final alwaysAliveProvider = AsyncNotifierProvider<AlwaysAliveNotifier, void>(AlwaysAliveNotifier.new);

// app.dart
@pragma('vm:entry-point')
Future<void> backgroundGeolocationHeadlessTask(bg.HeadlessEvent headlessEvent) async {
  if (headlessEvent.name == bg.Event.GEOFENCE) {
    try {
      final geofenceEvent = headlessEvent.event as bg.GeofenceEvent;
      await handleGeofenceEvent(geofenceEvent);
    } catch (e, st) {
      await MeryUtils.logMery('[HeadlessTask] ERROR processing Geofence: $e, $st');
    }
  }
}

Future<void> initializeApp(Flavor flavor) async {
  WidgetsFlutterBinding.ensureInitialized();
  await bg.BackgroundGeolocation.stop();
  await bg.BackgroundGeolocation.removeListeners();
  bg.BackgroundGeolocation.registerHeadlessTask(backgroundGeolocationHeadlessTask);
}

//geofence_event_handler.dart
Future<void> handleGeofenceEvent(bg.GeofenceEvent event) async {
  await MeryUtils.logMery(
      '[GEOFENCE HANDLER] handleGeofenceEvent ${event.action}');
   // show notifiction...

});

[Optional] Relevant log output


Merynek avatar Nov 18 '25 19:11 Merynek

One interesting new: Steps:

  1. Start app (run geofencing,.. -> it works)
  2. Kill app (no waiting for notification) And Start app again.
  3. It works (geofencing registered)

So its something wrong when run handleGeofenceEvent on background when app is killed. On that event I just show notification thats it. Any idea?

Merynek avatar Nov 18 '25 19:11 Merynek

I found out that this error is logged when notification shows (without click) so it not necessary to open app again.

Merynek avatar Nov 18 '25 19:11 Merynek

I am not an expert in flutter. Your code looks unusual to me.

.ready(config), as well as traditional event-listeners (.onLocation, onXXX, etc) are designed to be run only when the MainActivity is present (ie: inside a flutter App instance).

You appear to be violating those expectations with code outside the flutter App instance.

christocracy avatar Nov 18 '25 23:11 christocracy

its just code inside my App, I just want to show config etc...

Merynek avatar Nov 19 '25 07:11 Merynek

Look: in app instance I call "initializeApp" method. I have AlwaysAliveNotifier for services which I want to live during app is on. And I got this error when backgroundGeolocationHeadlessTask called. Notification appeared but after that error no geofence fired.

Merynek avatar Nov 19 '25 07:11 Merynek

I figured out that problem is position of my: bg.BackgroundGeolocation.onGeofence(onGeofence); Now I have it after ready promise like this (why its wrong?):

class GeofenceService {
  final Ref ref;

  GeofenceService(this.ref) {
    bg.BackgroundGeolocation.ready(bg.Config(
      enableHeadless: true,
      desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
      persistMode: bg.Config.PERSIST_MODE_GEOFENCE,
      notification: bg.Notification(
        title: "",
        text: "",
        priority: bg.Config.NOTIFICATION_PRIORITY_MIN,
        sticky: false,
      ),
      distanceFilter: 10.0,
      stopOnTerminate: false,
      foregroundService: true,
      geofenceModeHighAccuracy: true,
      startOnBoot: true,
      debug: false,
      geofenceInitialTriggerEntry: true,
      logLevel: bg.Config.LOG_LEVEL_OFF,
    )).then((bg.State state) async {
      bg.BackgroundGeolocation.onGeofence(onGeofence);
      if (!state.enabled) {
        await MeryUtils.logMery('[GeofenceService] READY');
        await bg.BackgroundGeolocation.start();
      }
      _listenChanges();
    });
  }

  Future<void> onGeofence(bg.GeofenceEvent event) async {
    try {
      await MeryUtils.logMery('[GeofenceService] onGeofence');
      await handleGeofenceEvent(event);
      ref.read(visitedGeofenceServiceProvider).triggerProcess();
    } catch (e, st) {
      await MeryUtils.logMery('[GeofenceService] ERROR onGeofence: $e, $st');
    }
  }

  void _listenChanges() {
    if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
      ref.listen(currentGeofencingProvider, (_, geofenceStopsAsyncValue) async {
        await _processGeofenceUpdate(geofenceStopsAsyncValue);
      }, fireImmediately: true);
    }
  }
}

Merynek avatar Nov 19 '25 09:11 Merynek

I tried add ensureInitialized and remove foregroundService and geofenceModeHighAccuracy but still logged an error.

class GeofenceService {
  final Ref ref;

  GeofenceService(this.ref) {
    WidgetsFlutterBinding.ensureInitialized();
    MeryUtils.logMery('[GeofenceService] CONSTRUCTOR');
    bg.BackgroundGeolocation.onGeofence(onGeofence);
    bg.BackgroundGeolocation.ready(bg.Config(
      enableHeadless: true,
      desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
      persistMode: bg.Config.PERSIST_MODE_GEOFENCE,
      notification: bg.Notification(
        title: "",
        text: "",
        priority: bg.Config.NOTIFICATION_PRIORITY_MIN,
        sticky: false,
      ),
      distanceFilter: 10.0,
      stopOnTerminate: false,
      startOnBoot: true,
      debug: false,
      geofenceInitialTriggerEntry: true,
      logLevel: bg.Config.LOG_LEVEL_OFF,
    )).then((bg.State state) async {
      await MeryUtils.logMery(
          '[GeofenceService] THEN enabled: ${state.enabled}');
      if (!state.enabled) {
        await MeryUtils.logMery('[GeofenceService] READY');
        await bg.BackgroundGeolocation.startGeofences();
      }
      _listenChanges();
    });
  }

logs after geofence is fired when app is killed:

[GeofenceService] CONSTRUCTOR
MissingPluginException: MissingPluginException(No implementation found for method listen on channel com.transistorsoft/flutter_background_geolocation/events/geofence)
[GeofenceService] THEN enabled: true

It looks that geofence revived the application. Any idea?

Merynek avatar Nov 19 '25 11:11 Merynek

Never invoke your GeofenceService when your app is headless (terminated).

When app is launched, you should add event listeners before calling .ready(config) (or you will miss events -- .ready(config) causes events to fire).

christocracy avatar Nov 19 '25 13:11 christocracy

and remove foregroundService and

foregroundService is documented as deprecated. The plug-in no longer respects that option (and hasn't for over 5 years). It is essentially hard-coded to true.

christocracy avatar Nov 19 '25 13:11 christocracy

Never invoke your GeofenceService when your app is headless (terminated).

When app is launched, you should add event listeners before calling .ready(config) (or you will miss events -- .ready(config) causes events to fire).

I think thats the problem. Any idea how can I detect if app is running in headless?

Merynek avatar Nov 19 '25 15:11 Merynek

Why do you need to detect if headless?

christocracy avatar Nov 19 '25 16:11 christocracy

Actually I dont know why this service is called in headless. I have this service in my alwaysAliveProvider (its riverpod hack how to maintain this alive state).

class AlwaysAliveNotifier extends AsyncNotifier<void> {
  @override
  Future<void> build() async {
    ref.watch(geofenceServiceProvider.select((_) => true));
  }
}
final alwaysAliveProvider = AsyncNotifierProvider<AlwaysAliveNotifier, void>(AlwaysAliveNotifier.new);

and this alwaysAliveProvider is in builder here in app.dart

return Consumer(
            builder: (context, ref, _) {
              return MaterialApp.router(
                builder: (context, widget) {
                  ref.watch(alwaysAliveProvider.select((_) => true));
                  if (kDebugMode) {
                    ErrorWidget.builder = (errorDetails) {
                      Widget error = Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: Text("$errorDetails", style: const TextStyle(fontSize: 12)),
                      );
                      if (widget is Scaffold || widget is Navigator) {
                        error = Scaffold(body: Center(child: error));
                      }
                      return error;
                    };
                  }
                  throw StateError('widget is null');
                }
              );
            },
          );

But still dont understand why its called in headless mode

Merynek avatar Nov 19 '25 16:11 Merynek

mby something turn on my app in headless callback. this is my callback:

@pragma('vm:entry-point')
Future<void> backgroundGeolocationHeadlessTask(bg.HeadlessEvent headlessEvent) async {
  if (headlessEvent.name == bg.Event.GEOFENCE) {
    try {
      await MeryUtils.logMery('[HeadlessTask] onGeofence HeadlessTask');
      final geofenceEvent = headlessEvent.event as bg.GeofenceEvent;
      await handleGeofenceEvent(geofenceEvent);
    } catch (e, st) {
      await MeryUtils.logMery('[HeadlessTask] ERROR processing Geofence: $e, $st');
    }
  }
}

final _geofenceWriteQueue = AsyncQueue.autoStart();

Future<void> handleGeofenceEvent(bg.GeofenceEvent event) async {
  _geofenceWriteQueue.addJob((_) async {
    try {
      await MeryUtils.logMery('[GEOFENCE HANDLER] handleGeofenceEvent ${event.action}');

      if (event.action != 'ENTER') {
        await MeryUtils.logMery('[GEOFENCE HANDLER] Action is not "ENTER"');
        return;
      }

      final identifier = event.identifier;
      if (identifier == null) {
        await MeryUtils.logMery('[GEOFENCE HANDLER] Geofence ID is null, skipping.');
        return;
      }

      final dataString = event.extras?['data'];
      if (dataString == null) {
        await MeryUtils.logMery('[GEOFENCE HANDLER] dataString is null');
        return;
      }

      GeofenceData? data;
      try {
        data = GeofenceData.fromJson(jsonDecode(dataString));
      } catch (e) {
        await MeryUtils.logMery('[GEOFENCE HANDLER] Failed to decode GeofenceData: $e');
        return;
      }

      if (data == null) {
        await MeryUtils.logMery('[GEOFENCE HANDLER] Data is null');
        return;
      }
      final location = event.location;
      final placeId = data.hotelInfo?.id ?? data.place!.placeId;
      final point = data.hotelInfo?.point ?? data.place!.point;
      final placeName = data.hotelInfo?.name ?? data.place!.name;
      final useHotelInfo = data.hotelInfo != null;
      final orderNumber = data.params.orderNumber;

      final sharedPrefs = SharedPreferencesRepository();
      final allVisitedGeofence = await sharedPrefs.getAllVisitedGeofenceData();
      if (allVisitedGeofence.any((geofence) => geofence.placeId == placeId && geofence.orderNumber == orderNumber)) {
        await MeryUtils.logMery('[GEOFENCE HANDLER] Place is already on allVisitedGeofence, skipping.');
        return;
      }

      final placeData = MyTripPlaceParams(
          placeId: placeId,
          orderNumber: orderNumber,
          useHotelInfo: data.hotelInfo != null,
          showArrivedAnimation: true,
          geofenceId: event.identifier ?? ""
      );

      try {
        await sharedPrefs.saveVisitedGeofenceData(placeData);
        await bg.BackgroundGeolocation.removeGeofence(identifier);
        await MeryUtils.logMery('[GEOFENCE HANDLER] data processing success.');
      } catch (e) {
        await MeryUtils.logMery('[GEOFENCE HANDLER] Save data to storage $e');
      }

      final plugin = FlutterLocalNotificationsPlugin();
      const geofenceNotificationId = 64;
      final isDev = data.flavor == Flavor.dev;
      final notificationChannelId = isDev ? 'cz.worldeecom.worldee.dev.geofence' : 'cz.worldeecom.worldee.geofence';
      const notificationChannelName = 'Worldee geofencing';

      final distance = location != null
          ? gl.Geolocator.distanceBetween(location.coords.latitude, location.coords.longitude, point.lat, point.lng)
          : null;

      final uri = Uri(
        scheme: data.flavor.urlScheme,
        host: data.flavor.urlHost,
        path: "${DeepLinkConst.myTrip}/$orderNumber",
        queryParameters: {DeepLinkConst.placeId: placeId, DeepLinkConst.useHotelInfo: useHotelInfo.toString()},
      );

      final payload = Uri.encodeComponent(uri.toString());

      await plugin.show(
        geofenceNotificationId,
        kDebugMode ? "order $orderNumber place $placeId" : data.getNotificationTitle(),
        kDebugMode
            ? "$placeName; dist $distance; cur: (${location.coords.latitude}, ${location.coords.longitude})"
            : data.getNotificationBody(distance),
        NotificationDetails(
          android: AndroidNotificationDetails(
            notificationChannelId,
            notificationChannelName,
            importance: Importance.max,
            priority: Priority.high,
          ),
          iOS: DarwinNotificationDetails(
            threadIdentifier: notificationChannelId,
            presentAlert: true,
            interruptionLevel: InterruptionLevel.timeSensitive,
          ),
        ),
        payload: payload,
      );
    } catch(e) {
      await MeryUtils.logMery('[GEOFENCE HANDLER] GLOBAL ERROR $e');
    }
    await MeryUtils.logMery('[GEOFENCE HANDLER] Data processing finished.');
  });
}

class MeryUtils {
  static Future<void> logMery(String message) async {
    debugPrint("Merynek loguje: $message");
  }
}

any idea?

Merynek avatar Nov 19 '25 16:11 Merynek

btw this is my main.dart ..I think that this is not problem, but for sure. :-)

@pragma('vm:entry-point')
Future<void> backgroundGeolocationHeadlessTask(bg.HeadlessEvent headlessEvent) async {
  if (headlessEvent.name == bg.Event.GEOFENCE) {
    try {
      await MeryUtils.logMery('[HeadlessTask] onGeofence HeadlessTask');
      final geofenceEvent = headlessEvent.event as bg.GeofenceEvent;
      await handleGeofenceEvent(geofenceEvent);
    } catch (e, st) {
      await MeryUtils.logMery('[HeadlessTask] ERROR processing Geofence: $e, $st');
    }
  }
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await bg.BackgroundGeolocation.registerHeadlessTask(backgroundGeolocationHeadlessTask);
  await initializeApp(Flavor.dev);

  // Disable leak tracking on iOS due to high memory consumption
  if (kDebugMode && !Platform.isIOS) {
    FlutterMemoryAllocations.instance.addListener(
      (ObjectEvent event) => LeakTracking.dispatchObjectEvent(event.toMap()),
    );
    LeakTracking.start();
  }

  Stripe.publishableKey = const String.fromEnvironment('STRIPE_TEST_ACCESS_TOKEN');

  debugPrint = (text, {wrapWidth}) {
    log(text ?? "-");
  };

  // FlutterError.onError = (details) {
  //   //TODO: Add error logging
  // };

  await SentryFlutter.init((options) {
    options.dsn = _devSentryDSN;
    // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
    // We recommend adjusting this value in production.
    options.tracesSampleRate = _devSentrySampleRate;
    options.ignoreErrors = ['InterceptorState*'];
    options.debug = false;
  }, appRunner: () => runApp(const App()));
}

Merynek avatar Nov 19 '25 17:11 Merynek

I found out that HeadlessTask run my main app as well. Thats why my services was inited and thats the problem In my logs I saw that app was started before run handleGeofenceEvent. But I dont want this bcs it makes errors.. I dont know why its run whole app in headless.

main.dart

@pragma('vm:entry-point')
Future<void> backgroundGeolocationHeadlessTask(bg.HeadlessEvent headlessEvent) async {
  if (headlessEvent.name == bg.Event.GEOFENCE) {
    final geofenceEvent = headlessEvent.event as bg.GeofenceEvent;
    await handleGeofenceEvent(geofenceEvent);
  }
}

Future<void> main() async {
  SentryWidgetsFlutterBinding.ensureInitialized();
  bg.BackgroundGeolocation.registerHeadlessTask(backgroundGeolocationHeadlessTask);
  await initializeApp(Flavor.dev);
  runApp(const App());
}

Merynek avatar Nov 20 '25 13:11 Merynek

I created simple app only with create headless task. When I terminate an app and change my location to registred geofence my headlessTask fired but main as well. So I dont understand. How can I catch if app si running in headless bcs it start whole my app and I dont want this.

any idea? @christocracy

Merynek avatar Nov 21 '25 10:11 Merynek

FYI: I fix this but I think that its not propper fix :-( Never render app in background. In app:

class App extends StatefulWidget {
  const App({super.key});

  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App>
    with WidgetsBindingObserver {
  bool _isRunningInBackground = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _isRunningInBackground = WidgetsBinding.instance.lifecycleState == AppLifecycleState.detached;
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if ((state == AppLifecycleState.detached) != _isRunningInBackground) {
      setState(() {
        _isRunningInBackground = state == AppLifecycleState.detached;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_isRunningInBackground) {
      return const SimpleAppContent();
    }
    return const MyApp();
  }
}

class SimpleAppContent extends StatelessWidget {
  const SimpleAppContent({super.key});
  @override
  Widget build(BuildContext context) {
      // return empty widget
      return const MaterialApp(
        title: 'simple',
        home: Scaffold(
          backgroundColor: Colors.white,
          body: Center(
            child: Text('SimpleAppContent is running in background'),
          ),
        ),
        debugShowCheckedModeBanner: false,
      );
  }
}

It works but.... @christocracy

Merynek avatar Dec 17 '25 11:12 Merynek