You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 04:35:12