Flutter后台定位追踪实现及离线数据存储方案求助
问题描述
我是一名17岁的新手应用开发者,开发了一款Flutter应用,管理员可在地图上保存经纬度创建路线,供用户查找钓鱼兴趣点。但遇到锁屏或手机放进口袋后,应用停止追踪路线的问题,得知可通过后台服务解决。
以下是我的追踪代码:
class TrackRouteScreen extends StatefulWidget { final String id; TrackRouteScreen({required this.id}); @override State<TrackRouteScreen> createState() => _TrackRouteScreenState(); } class _TrackRouteScreenState extends State<TrackRouteScreen> { bool isTracking = false; final CollectionReference routesCollection = FirebaseFirestore.instance.collection('Fish POI'); late MapController mapController; LatLng? currentPosition; List<LatLng> polylineCoordinates = []; List<Marker> markers = []; StreamSubscription<Position>? positionStreamSubscription; @override void initState() { initialize(); super.initState(); } Future<void> initialize() async { mapController = MapController(); currentPosition = await getCurrentLocation(); setState(() {}); } // Get current location of the user Future<LatLng> getCurrentLocation() async { bool serviceEnabled; LocationPermission permission; serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { throw Exception('Location services are disabled'); } permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission==LocationPermission.denied||permission == LocationPermission.deniedForever) { throw Exception('Location permissions are denied'); } } if (permission == LocationPermission.deniedForever) { throw Exception('Location permissions are permanently denied'); } Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high ); return LatLng(position.latitude, position.longitude); } // Start tracking void startTracking() { setState(() { isTracking = true; polylineCoordinates.clear(); }); positionStreamSubscription = Geolocator.getPositionStream( locationSettings: LocationSettings()).listen((Position position) { updateLocation(position); } ); } // Update location and polyline void updateLocation(Position position) { setState(() { currentPosition = LatLng(position.latitude, position.longitude); polylineCoordinates.add(currentPosition!); }); } // Stop tracking void stopTracking() { setState(() { isTracking = false; }); positionStreamSubscription!.cancel(); saveTrackedRoute(polylineCoordinates); } // Save tracked route to Firestore DB Future<void> saveTrackedRoute(List<LatLng> trackedRoute) async { try { final DocumentReference routeDocRef = routesCollection.doc(widget.id); await routeDocRef.update({ 'route': trackedRoute .map((latLng) => GeoPoint(latLng.latitude, latLng.longitude)) .toList(), }); Navigator.push( context, MaterialPageRoute(builder: (context) => AddImagesScreen(id: widget.id)), ); } catch (error) { print('Error saving tracked route: $error'); } } @override Widget build(BuildContext context) { return Scaffold(body: body()); } Widget body() { return Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: Stack( children: [ FlutterMap( mapController: mapController, options: MapOptions(center: currentPosition, zoom: 15), children: [ TileLayer( urlTemplate: mapboxUrlTemplate, additionalOptions: { 'accessToken': mapboxAccessToken, 'id': 'mapbox.mapbox-streets-v8', }, ), PolylineLayer( polylines: [ Polyline( points: polylineCoordinates, strokeWidth: 5, color: Colors.blue, ), ], ), CurrentLocationLayer( followOnLocationUpdate: FollowOnLocationUpdate.always, style: LocationMarkerStyle( marker: DefaultLocationMarker(), ), ), ], ), Padding( padding: EdgeInsets.symmetric(horizontal: 15, vertical: 20), child: Align( alignment: Alignment.bottomCenter, child: GradientElevatedButton( onTap: isTracking ? stopTracking : startTracking, width: MediaQuery.of(context).size.width, beginningColor: isTracking ? Colors.red : Colors.green, endingColor: isTracking ? Colors.red : Colors.green, text: isTracking ? 'Stop' : 'Start', ), ), ), Positioned( top: 40.0, right: 15.0, child: FloatingActionButton( onPressed: () { if (isTracking) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('Quit Tracking'), content: Text('Are you sure you want to quit tracking?'), actions: <Widget>[ TextButton( child: Text('Cancel'), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( child: Text('Quit'), onPressed: () { Navigator.of(context).pop(); Navigator.of(context).pop(); stopTracking(); }, ), ], ); }, ); } else { Navigator.pop(context); } }, child: Icon(Icons.close), backgroundColor: Colors.grey[300], foregroundColor: Colors.black, ), ), ], ), ); } }
我对后台追踪的概念感到困惑,想了解其含义及实现方法;同时担心无网络时数据存储位置,希望有人能帮我理解相关概念、修改代码并简单解释,推荐合适的开发包。
解决方案
一、后台追踪概念解释
普通位置监听仅在App前台运行时生效,当App被锁屏、切到后台或放入口袋时,系统会限制App资源使用,导致监听停止。后台追踪是让App在后台甚至锁屏状态下,持续获取位置信息的能力,需要系统特殊权限和专门的后台服务支持。
二、推荐开发包
- geolocator:你已在使用,支持后台位置监听,需配置平台权限。
- workmanager:用于在Android/iOS调度后台任务,即使App被杀死也能触发位置采集。
- hive:轻量级本地数据库,适合离线存储路线数据,网络恢复后同步到Firestore。
三、代码修改步骤
1. 添加依赖
在pubspec.yaml中新增:
dependencies: geolocator: ^10.1.0 workmanager: ^0.5.1 hive: ^2.2.3 hive_flutter: ^1.1.0
2. 配置平台权限
- Android:在
AndroidManifest.xml中添加:
<!-- 后台位置权限 --> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- 唤醒锁权限 --> <uses-permission android:name="android.permission.WAKE_LOCK" /> <!-- 开机自启权限 --> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
- iOS:在
Info.plist中添加:
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <string>需要持续获取位置以追踪钓鱼路线</string> <key>NSLocationAlwaysUsageDescription</key> <string>需要在后台获取位置以追踪钓鱼路线</string> <key>UIBackgroundModes</key> <array> <string>location</string> </array>
3. 修改追踪代码
import 'package:hive/hive.dart'; import 'package:workmanager/workmanager.dart'; import 'package:geolocator/geolocator.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; // 初始化Hive本地数据库 void initHive() async { await Hive.initFlutter(); await Hive.openBox('routeBox'); } class TrackRouteScreen extends StatefulWidget { final String id; TrackRouteScreen({required this.id}); @override State<TrackRouteScreen> createState() => _TrackRouteScreenState(); } class _TrackRouteScreenState extends State<TrackRouteScreen> { bool isTracking = false; final CollectionReference routesCollection = FirebaseFirestore.instance.collection('Fish POI'); late MapController mapController; LatLng? currentPosition; List<LatLng> polylineCoordinates = []; StreamSubscription<Position>? positionStreamSubscription; late Box routeBox; @override void initState() { initialize(); super.initState(); } Future<void> initialize() async { mapController = MapController(); routeBox = Hive.box('routeBox'); // 加载本地未完成的路线 loadOfflineRoute(); currentPosition = await getCurrentLocation(); setState(() {}); } // 加载本地存储的路线数据 void loadOfflineRoute() { final savedRoute = routeBox.get(widget.id, defaultValue: []); if (savedRoute is List) { polylineCoordinates = savedRoute.map((e) => LatLng(e['lat'], e['lng'])).toList(); } } // 原getCurrentLocation方法保留 Future<LatLng> getCurrentLocation() async { bool serviceEnabled; LocationPermission permission; serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { throw Exception('Location services are disabled'); } permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission==LocationPermission.denied||permission == LocationPermission.deniedForever) { throw Exception('Location permissions are denied'); } } if (permission == LocationPermission.deniedForever) { throw Exception('Location permissions are permanently denied'); } Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high ); return LatLng(position.latitude, position.longitude); } // 启动追踪 void startTracking() { setState(() { isTracking = true; polylineCoordinates.clear(); }); // 注册后台位置采集任务 Workmanager().registerPeriodicTask( 'locationTracking', 'trackLocation', frequency: const Duration(minutes: 1), // 每分钟采集一次,可根据需求调整 initialDelay: const Duration(seconds: 5), inputData: {'routeId': widget.id}, ); // 前台监听+后台任务双保险 positionStreamSubscription = Geolocator.getPositionStream( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, distanceFilter: 10, // 移动10米才更新位置 ) ).listen((Position position) { updateLocation(position); }); } // 更新位置并保存到本地 void updateLocation(Position position) { setState(() { currentPosition = LatLng(position.latitude, position.longitude); polylineCoordinates.add(currentPosition!); }); saveToLocal(); } // 保存路线到本地Hive void saveToLocal() { final routeData = polylineCoordinates.map((latLng) => { 'lat': latLng.latitude, 'lng': latLng.longitude, }).toList(); routeBox.put(widget.id, routeData); } // 停止追踪 void stopTracking() { setState(() { isTracking = false; }); positionStreamSubscription?.cancel(); // 取消后台任务 Workmanager().cancelByUniqueName('locationTracking'); // 同步本地数据到Firestore syncLocalToFirestore(); } // 同步本地数据到Firestore Future<void> syncLocalToFirestore() async { try { final DocumentReference routeDocRef = routesCollection.doc(widget.id); await routeDocRef.update({ 'route': polylineCoordinates.map((latLng) => GeoPoint(latLng.latitude, latLng.longitude)).toList(), }); // 同步成功后清除本地缓存 routeBox.delete(widget.id); Navigator.push( context, MaterialPageRoute(builder: (context) => AddImagesScreen(id: widget.id)), ); } catch (error) { print('Error syncing route: $error'); } } @override Widget build(BuildContext context) { return Scaffold(body: body()); } Widget body() { // 原body代码保留,无需修改 return Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: Stack( children: [ FlutterMap( mapController: mapController, options: MapOptions(center: currentPosition, zoom: 15), children: [ TileLayer( urlTemplate: mapboxUrlTemplate, additionalOptions: { 'accessToken': mapboxAccessToken, 'id': 'mapbox.mapbox-streets-v8', }, ), PolylineLayer( polylines: [ Polyline( points: polylineCoordinates, strokeWidth: 5, color: Colors.blue, ), ], ), CurrentLocationLayer( followOnLocationUpdate: FollowOnLocationUpdate.always, style: LocationMarkerStyle( marker: DefaultLocationMarker(), ), ), ], ), Padding( padding: EdgeInsets.symmetric(horizontal: 15, vertical: 20), child: Align( alignment: Alignment.bottomCenter, child: GradientElevatedButton( onTap: isTracking ? stopTracking : startTracking, width: MediaQuery.of(context).size.width, beginningColor: isTracking ? Colors.red : Colors.green, endingColor: isTracking ? Colors.red : Colors.green, text: isTracking ? 'Stop' : 'Start', ), ), ), Positioned( top: 40.0, right: 15.0, child: FloatingActionButton( onPressed: () { if (isTracking) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('Quit Tracking'), content: Text('Are you sure you want to quit tracking?'), actions: <Widget>[ TextButton( child: Text('Cancel'), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( child: Text('Quit'), onPressed: () { Navigator.of(context).pop(); Navigator.of(context).pop(); stopTracking(); }, ), ], ); }, ); } else { Navigator.pop(context); } }, child: Icon(Icons.close), backgroundColor: Colors.grey[300], foregroundColor: Colors.black, ), ), ], ), ); } } // 后台任务回调函数 void callbackDispatcher() { Workmanager().executeTask((task, inputData) async { if (task == 'trackLocation') { try { Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high); Box routeBox = await Hive.openBox('routeBox'); String? routeId = inputData?['routeId']; if (routeId != null) { List savedRoute = routeBox.get(routeId, defaultValue: []); savedRoute.add({'lat': position.latitude, 'lng': position.longitude}); routeBox.put(routeId, savedRoute); } } catch (e) { print('Background tracking error: $e'); } } return Future.value(true); }); }
4. 初始化后台任务和Hive
在main.dart中添加初始化逻辑:
void main() async { WidgetsFlutterBinding.ensure
相关产品推荐
相关产品推荐

