MissingPluginException when app turn on again
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.
- Start app (run geofencing,.. -> it works)
- Kill app (wait for notification from OS)
- 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)
- 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
One interesting new: Steps:
- Start app (run geofencing,.. -> it works)
- Kill app (no waiting for notification) And Start app again.
- 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?
I found out that this error is logged when notification shows (without click) so it not necessary to open app again.
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.
its just code inside my App, I just want to show config etc...
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.
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);
}
}
}
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?
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).
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.
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?
Why do you need to detect if headless?
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
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?
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()));
}
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());
}
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
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