0

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

Background Modes

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

1
  • You probably aren't processing background notifications on IOS correctly. You don't have "background services" on iOS for this sort of thing. There is just a stream of location updates presented to the location delegate; I don't know how the Flutter plug in exposes these. Your symptoms sound like you are requiring constant background execution of your app; This is unlimited when running under the debugger but not available when running release code Commented Aug 12 at 7:26

1 Answer 1

-1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.