I’m using Flutter with flutter_background_service and geolocator to get location updates in the background.
In debug mode, background location works fine even when the app is minimized or the screen is locked.
But when running in release mode or installing the app via TestFlight, it stops sending any background location updates.
I have enabled the following in Xcode
info.plist
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>location</string>
<string>remote-notification</string>
<string>processing</string>
</array>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Your location is needed even when the app is in the background to track trips.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Your location is needed even when the app is in the background to track trips.</string>
<key>NSLocationTemporaryUsageDescriptionDictionary</key>
<dict>
<key>LocationTracking</key>
<string>This app needs precise location for school bus tracking and safety monitoring purposes.</string>
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location is needed to track trips while the app is open.</string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>dev.flutter.background.refresh</string>
</array>
BackgorundService
class BackgroundServiceController extends ChangeNotifier {
static final BackgroundServiceController _instance =
BackgroundServiceController._internal();
factory BackgroundServiceController() => _instance;
BackgroundServiceController._internal();
final FlutterBackgroundService _service = FlutterBackgroundService();
Future<void> initializeService({
required int? disInterval,
required int? timeInterval,
}) async {
SessionController().setDistanceAndTimeInterval(
disInterval: disInterval.toString(),
timeInterval: timeInterval.toString(),
);
final isRunning = await _service.isRunning();
if (isRunning) {
await Future.delayed(const Duration(seconds: 1));
_service.invoke('setData');
return;
}
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'my_foreground',
'MY FOREGROUND SERVICE',
description:
'This channel is used for important notifications.',
importance: Importance.low,
);
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
if (Platform.isIOS || Platform.isAndroid) {
await flutterLocalNotificationsPlugin.initialize(
const InitializationSettings(
iOS: DarwinInitializationSettings(),
android: AndroidInitializationSettings('logo'),
),
);
}
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.createNotificationChannel(channel);
await _service.configure(
androidConfiguration: AndroidConfiguration(
onStart: onStart,
autoStart: true,
isForegroundMode: true,
autoStartOnBoot: true,
notificationChannelId: 'my_foreground',
initialNotificationTitle: 'Location Service',
initialNotificationContent: 'Initializing...',
foregroundServiceNotificationId: 888,
foregroundServiceTypes: [AndroidForegroundType.location],
),
iosConfiguration: IosConfiguration(
autoStart: true,
onForeground: onStart,
onBackground: onIosBackground,
),
);
try {
await _service.startService();
} catch (e) {
}
while (!(await _service.isRunning())) {
await Future.delayed(const Duration(milliseconds: 200));
}
await Future.delayed(const Duration(seconds: 3));
_service.invoke('setData');
}
Future<void> stopService() async {
final isRunning = await _service.isRunning();
if (isRunning) {
_service.invoke("stopService");
} else {
}
}
Future<bool> isServiceRunning() async {
return await _service.isRunning();
}
}
@pragma('vm:entry-point')
void onStart(ServiceInstance service) async {
// DartPluginRegistrant.ensureInitialized();
WidgetsFlutterBinding.ensureInitialized();
await AppConstants.init();
await SessionController().init();
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
final disInterval = SessionController().disInterval ?? 20;
final timeInterval = SessionController().timeInterval ?? 10;
StreamSubscription<Position>? positionStream;
final homeViewModel = HomeViewModel();
void startLocationTracking() async {
if (positionStream != null) {
return;
}
DateTime? lastSentTime;
positionStream =
Geolocator.getPositionStream(
locationSettings: const LocationSettings(
distanceFilter: 0,
accuracy: LocationAccuracy.bestForNavigation,
),
).listen((position) async {
final now = DateTime.now();
if (lastSentTime == null ||
now.difference(lastSentTime!).inSeconds >= (timeInterval)) {
lastSentTime = now;
try {
await homeViewModel.pushLiveLocation(position: position);
} catch (e) {
}
} else {
}
});
}
service.on('stopService').listen((event) async {
await positionStream?.cancel();
positionStream = null;
await service.stopSelf();
});
service.on('setData').listen((data) async {
final disInterval = SessionController().disInterval ?? 20;
final timeInterval = SessionController().timeInterval ?? 10;
await Future.delayed(const Duration(seconds: 5));
startLocationTracking();
});
if (service is AndroidServiceInstance &&
await service.isForegroundService()) {
flutterLocalNotificationsPlugin.show(
888,
'Tracking location in background',
'Background location is on to keep the app up-tp-date with your location. This is required for main features to work properly when the app is not running.',
const NotificationDetails(
android: AndroidNotificationDetails(
'my_foreground',
'MY FOREGROUND SERVICE',
icon: 'ic_stat_notification',
ongoing: true,
styleInformation: BigTextStyleInformation(
'Background location is on to keep the app up-to-date with your location. '
'This is required for main features to work properly when the app is not running.',
contentTitle: 'Tracking location in background',
htmlFormatContent: true,
),
),
),
);
// service.setForegroundNotificationInfo(
// title: "Location Tracking",
// content: "Tracking your location in background",
// );
}
}
@pragma('vm:entry-point')
Future<bool> onIosBackground(ServiceInstance service) async {
// DartPluginRegistrant.ensureInitialized();
WidgetsFlutterBinding.ensureInitialized();
await AppConstants.init();
await SessionController().init();
final homeViewModel = HomeViewModel();
try {
final disInterval = SessionController().disInterval ?? 20;
final sub =
Geolocator.getPositionStream(
locationSettings: const LocationSettings(
distanceFilter: 0,
accuracy: LocationAccuracy.bestForNavigation,
),
).listen((position) async {
try {
await homeViewModel.pushLiveLocation(position: position);
} catch (e) {
}
});
await Future.delayed(const Duration(seconds: 30));
await sub.cancel();
} catch (e) {
}
return true;
}
I also checked that the app has Always Allow
location permission in iOS Settings
The reason background location
updates work in debug
mode but not in release
/TestFlight
is that in release builds, iOS enforces stricter background execution rules. If your CLLocationManager
is not configured correctly, iOS will stop sending updates when the app goes to the background.
Here’s the updated AppDelegate
configuration that fixed the issue:
import Flutter
import UIKit
import CoreLocation
@main
@objc class AppDelegate: FlutterAppDelegate, CLLocationManagerDelegate {
var locationManager: CLLocationManager?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
locationManager = CLLocationManager()
locationManager?.delegate = self
locationManager?.allowsBackgroundLocationUpdates = true
locationManager?.pausesLocationUpdatesAutomatically = false
locationManager?.requestAlwaysAuthorization()
locationManager?.startUpdatingLocation()
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
With this configuration, background location updates worked consistently in release builds.