I’m building a Flutter app that needs to run continuously in the background to track whether the user is at the gym and keep a workout timer running. Flutter background execution stops after some time (timer & location monitoring not working)
The issue:
I’m already using:
flutter_backgroundgeolocatorpermission_handlerBut still, the execution eventually stops.
Here is the main part of the implementation (simplified but showing where background execution is set up):
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_background/flutter_background.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:latlong2/latlong.dart';
import 'package:geolocator/geolocator.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_background/src/android_config.dart' as fb;
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool running = false;
Duration elapsed = Duration.zero;
LatLng? gymLocation;
static const double gymRadiusMeters = 35.0;
bool inGym = false;
Timer? _locationTimer;
Timer? _statusTimer;
static bool _backgroundStarted = false;
@override
void initState() {
super.initState();
_setupApp();
_ensureBackgroundServiceStarted();
}
Future<void> _ensureBackgroundServiceStarted() async {
if (!_backgroundStarted) {
_backgroundStarted = true;
await _requestPermissions();
await _startBackgroundMode();
await _loadGymLocation();
_startPersistentBackgroundLocationMonitor();
if (await Permission.ignoreBatteryOptimizations.isDenied) {
await Permission.ignoreBatteryOptimizations.request();
}
}
}
void _startPersistentBackgroundLocationMonitor() {
Timer.periodic(const Duration(seconds: 30), (_) async {
final prefs = await SharedPreferences.getInstance();
final lat = prefs.getDouble('gym_lat');
final lng = prefs.getDouble('gym_lng');
if (lat == null || lng == null) return;
final gymLoc = LatLng(lat, lng);
try {
final pos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.low,
timeLimit: const Duration(seconds: 10),
);
final Distance distance = Distance();
final double dist = distance(
LatLng(pos.latitude, pos.longitude),
gymLoc,
);
final bool inside = dist <= gymRadiusMeters;
setState(() {
inGym = inside;
});
} catch (e) {
// fallback to last known location
}
});
}
Future<void> _startBackgroundMode() async {
final hasPermissions = await FlutterBackground.hasPermissions;
if (!hasPermissions) {
await FlutterBackground.initialize(
androidConfig: fb.FlutterBackgroundAndroidConfig(
notificationTitle: "GymSync Active",
notificationText: "Tracking your workout and location in the background.",
notificationIcon: fb.AndroidResource(name: 'ic_notification'),
enableWifiLock: true,
showBadge: true,
),
);
}
try {
await FlutterBackground.enableBackgroundExecution();
} catch (_) {}
}
Future<void> _requestPermissions() async {
if (await Permission.location.isDenied) {
await Permission.location.request();
}
if (await Permission.activityRecognition.isDenied) {
await Permission.activityRecognition.request();
}
}
@override
void dispose() {
_locationTimer?.cancel();
_statusTimer?.cancel();
FlutterBackground.disableBackgroundExecution();
super.dispose();
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(child: Text("Gym Tracker")),
);
}
}
How can I keep this service running truly in the background on Android (and ideally iOS too)?
Do I need a proper background isolate, a WorkManager, or a foreground service?
What is the correct approach to ensure timers + location monitoring won’t be killed when the app goes to the background?
flutter_background package's README says:
A plugin to keep flutter apps running in the background. Currently only works with Android.
It achieves this functionality by running an Android foreground service in combination with a partial wake lock and disabling battery optimizations in order to keep the flutter isolate running.
Unfortunately this package doesn't support iOS but perhaps in the future they could enable it by connecting the required method channels. And even if it supported iOS it wouldn't solve your issue because isolates don't survive the app termination command, they are still in the application and will be terminated when the app is terminated.
Example of what we want:
Think of Windows's services or a process that runs on a UNIX system that you can detach from a terminal and keep it's life-cycle alive, that's what you are aiming and in-order to do that I recommend the following package:
background_fetch is another package that you can take a look at:
Background Fetch is a very simple plugin which will awaken an app in the background about every 15 minutes, providing a short period of background running-time. This plugin will execute your provided
callbackFnwhenever a background-fetch event occurs.
My personal recommendation and experience with some area specific work in parking area industry would be 15-20 minutes is good enough of a precision for a location tracker. I hope this experience will help you out as well.
Let's move onto the implementation part.
The Implementation:
background_fetch: ^1.4.0
geolocator: ^14.0.2
latlong2: ^0.9.1
shared_preferences: ^2.5.3
main.dartimport 'package:flutter/material.dart';
import 'package:background_fetch/background_fetch.dart';
import 'home_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Configure background_fetch
BackgroundFetch.configure(
BackgroundFetchConfig(
minimumFetchInterval: 15, // every 15 min (iOS min)
stopOnTerminate: false, // keep running after app terminate
enableHeadless: true, // allow Android headless execution
),
(String taskId) async {
// This runs in the background
await HomeScreen.checkGymLocationInBackground();
BackgroundFetch.finish(taskId);
},
(String taskId) async {
// Task timeout handler
BackgroundFetch.finish(taskId);
},
);
// Start background fetch service
BackgroundFetch.start();
runApp(const MaterialApp(home: HomeScreen()));
}
HomeScreenInstead of a timer in an Isolate ,which will be terminated on app termination on iOS, use this instead:
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
import 'package:shared_preferences/shared_preferences.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
static Future<void> checkGymLocationInBackground() async {
final prefs = await SharedPreferences.getInstance();
final lat = prefs.getDouble('gym_lat');
final lng = prefs.getDouble('gym_lng');
if (lat == null || lng == null) return;
final gymLoc = LatLng(lat, lng);
final distanceCalc = Distance();
try {
final pos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.low,
timeLimit: const Duration(seconds: 10),
);
final dist = distanceCalc(LatLng(pos.latitude, pos.longitude), gymLoc);
final inside = dist <= 35.0;
// Save status to prefs (or notify server, etc.)
await prefs.setBool('in_gym', inside);
} catch (_) {
// ignore errors for now
}
}
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool inGym = false;
@override
void initState() {
super.initState();
_loadInitialStatus();
}
Future<void> _loadInitialStatus() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
inGym = prefs.getBool('in_gym') ?? false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text(inGym ? "Inside Gym" : "Outside Gym")),
);
}
}
void backgroundFetchHeadlessTask(HeadlessTask task) async {
String taskId = task.taskId;
bool timeout = task.timeout;
if (timeout) {
BackgroundFetch.finish(taskId);
return;
}
await HomeScreen.checkGymLocationInBackground();
BackgroundFetch.finish(taskId);
}
// Register headless task
BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
In a nutshell:
The flutter app will periodically wake up every 15 minutes
HomeScreen.checkGymLocationInBackground() runs in the background, like a service.
You can persist results to SharedPreferences or even send to your RESTful API. You can use GetStorage if you prefer that.
Unlike isolates, this survives app termination and device reboot.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With