I’m building a Flutter app which will use EventChannel and MethodChannel that will communicate between Native and Flutter code. I’m using the IOS Region Monitoring to detect if the user have exited or entered the region.
On the Flutter side, I will check for all necessary permissions before passing the regions to the Native side through Method Channel for region monitoring and uses EventChannel to inform the Flutter side that the user have either exited or entered the region. Everything works as expected when the app is in alive in foreground or in the background. However, when I terminate the app, the region monitoring doesn’t seem to work anymore.
I tried this prototype which is fully native, it works perfectly on foreground, background and terminated state.
Below is my code on the Native side
import UIKit
import CoreLocation
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
var locationManager: CLLocationManager!
var eventSink: FlutterEventSink?
var regions: [CLCircularRegion] = []
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(name: "location", binaryMessenger: controller.binaryMessenger)
let eventChannel = FlutterEventChannel(name: "location_stream", binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler(handle)
eventChannel.setStreamHandler(self)
locationManager = CLLocationManager()
locationManager.delegate = self
GeneratedPluginRegistrant.register(with: self)
if launchOptions?[UIApplication.LaunchOptionsKey.location] != nil {
NSLog("regrion monitoring: launchoptionkey")
if locationManager == nil {
locationManager = CLLocationManager()
locationManager.delegate = self
startMonitoring()
}
eventSink?(["type": 2, "identifier": "woke up"])
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
print("regrion monitoring: here")
switch call.method {
case "startMonitoring":
startMonitoring()
result(nil)
case "stopMonitoring":
stopMonitoring()
result(nil)
case "setRegions":
setRegions(call: call)
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
private func startMonitoring() {
print("regrion monitoring: start")
eventSink?(["type": 3, "identifier": "startMonitoring"])
for o in regions {
eventSink?(["type": 3, "identifier": "startMonitoring: (o.identifier)"])
print("regrion monitoring: (o.identifier)")
o.notifyOnExit = true;
o.notifyOnEntry = true;
locationManager.startMonitoring(for: o)
}
}
private func stopMonitoring() {
print("regrion monitoring: stop")
for o in regions {
locationManager.stopMonitoring(for: o)
}
// locationManager.delegate = nil
eventSink = nil
}
private func setRegions(call: FlutterMethodCall) {
let argResult = call.arguments as? [String: [String: Any]]
for s in argResult!.keys {
print("regrion monitoring: (s)");
regions.append(CLCircularRegion(
center: CLLocationCoordinate2D(latitude: argResult![s]!["latitude"] as! Double, longitude: argResult![s]!["longitude"] as! Double),
radius: argResult![s]!["radius"] as! Double,
identifier: argResult![s]!["description"] as! String))
}
}
private func handleEvent(forRegion region: CLRegion, type: Int) {
print("regrion monitoring: (region.identifier)")
eventSink?(["type": type, "identifier": region.identifier])
print("regrion monitoring: after event sink")
}
}
extension AppDelegate: CLLocationManagerDelegate {
public func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
print("regrion monitoring: didStartMonitoring")
print("(region.identifier)")
}
public func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
print("regrion monitoring: Enter")
if region is CLCircularRegion {
handleEvent(forRegion: region, type: 0)
}
}
public func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
print("regrion monitoring: Exit")
if region is CLCircularRegion {
handleEvent(forRegion: region, type: 1)
}
}
}
extension AppDelegate: FlutterStreamHandler {
@objc public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
print("onListen")
self.eventSink = events
return nil
}
@objc public func onCancel(withArguments arguments: Any?) -> FlutterError? {
print("onCancel")
self.eventSink = nil
return nil
}
}
Flutter code as below:
main.dart
import 'package:flutter/material.dart';
import 'package:regrion_monitoring/location.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
@override
void initState() {
super.initState();
requestLocationService().then((value) async {
if (value) {
await requestLocationPermission();
}
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Region Monitoring')),
body: Center(
child: Column(children: [
FilledButton(
onPressed: () async => await LocationHelper().setRegions(),
child: const Text('Set Region')),
FilledButton(
onPressed: () async => await LocationHelper().start(),
child: const Text('Start Monitor')),
FilledButton(
onPressed: () async => await LocationHelper().stop(),
child: const Text('Stop Monitor'))
]))));
}
}
location.dart
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:geolocator/geolocator.dart';
import 'package:location/location.dart' as location;
import 'package:permission_handler/permission_handler.dart' as permission;
class LocationHelper {
static final LocationHelper _instance = LocationHelper._internal();
LocationHelper._internal();
factory LocationHelper() => _instance;
final MethodChannel channel = const MethodChannel('location');
final EventChannel eventChannel = const EventChannel('location_stream');
Future<void> start() async {
print('regrion monitoring: start');
await channel.invokeMethod('startMonitoring');
eventChannel.receiveBroadcastStream().listen((event) {
Map<String, dynamic> result = Map<String, dynamic>.from(event as Map);
print('receiveBroadcastStream');
print('regrion monitoring: identifier: ${result['identifier']}');
print('regrion monitoring: type: ${result['type']}');
});
}
Future<void> stop() async {
print('regrion monitoring: stop');
await channel.invokeMethod('stopMonitoring');
}
Future<void> setRegions() async {
Map<String, dynamic> regions = {
'location1': {
'latitude': 3.1067979,
'longitude': 101.4466574,
'radius': 60,
'description': 'Lotus'
},
'location2': {
'latitude': 3.1072308,
'longitude': 101.4387836,
'radius': 60,
'description': 'NSK'
};
print('regrion monitoring: setRegions');
await channel.invokeMethod('setRegions', regions);
}
}
Future<bool> requestLocationService() async {
if (!await Geolocator.isLocationServiceEnabled()) {
if (Platform.isAndroid) {
await location.Location().requestService();
if (!await location.Location().serviceEnabled()) {
return false;
}
} else if (Platform.isIOS || Platform.isMacOS) {
await Geolocator.openLocationSettings();
}
}
return true;
}
Future<bool> requestLocationPermission() async {
if (await permission.Permission.locationWhenInUse.isDenied) {
await permission.Permission.locationWhenInUse.request();
}
if (await permission.Permission.locationWhenInUse.isGranted &&
!await permission.Permission.locationAlways.isGranted) {
if (Platform.isIOS || Platform.isMacOS) {
await permission.Permission.locationAlways.request();
}
if (await permission.Permission.locationAlways.isDenied) {
return false;
}
}
// if location permission is permanently denied
if (await permission.Permission.locationWhenInUse.isPermanentlyDenied) {
await permission.openAppSettings();
}
return true;
}
I have also set these two properties (Location Always and When In Use
and Location Always and When In Use
) in info.plist
I have also tried calling startMonitoring
in applicationWillTerminate
but it still doesn’t work. Can anyone help me?