《地图与定位服务:地图 SDK、定位与地理围栏》

完整梳理 Flutter 地图与定位能力:google_maps_flutter 与 flutter_map 的选型、geolocator 的权限与定位流、标记与折线绘制、地理围栏与轨迹记录,以及后台定位、耗电控制与常见踩坑,附平台配置与可复用 Dart 代码片段。

开篇:地图上那个"我在哪"的小蓝点不动了

打车、外卖、社交打卡类应用几乎都离不开地图与定位。上线后最常见的反馈是:“定位图标一直转圈"“在室内定位飘到几百米外"“应用切到后台就不更新位置了”。这些问题的根源分布在三层:系统权限、定位策略、地图 SDK 的渲染与配额。

Flutter 的地图与定位同样依赖插件:地图渲染用 google_maps_flutter 或 flutter_map,定位用 geolocator。地图本质上是原生视图(Platform View),定位本质上是系统位置服务,两者都有各自的配额、权限与耗电约束。本文沿着能力选型、地图接入、定位与权限、覆盖物绘制、地理围栏、性能踩坑这条链路展开。


一、地图与定位能力全景

1.1 地图 SDK 选型

方案渲染方式国内可用自定义能力典型场景
google_maps_flutter原生 Platform View需合规处理中海外应用
高德 Flutter 插件原生 Platform View是高国内出行
百度 Flutter 插件原生 Platform View是高国内导航
flutter_map纯 Dart + 瓦片取决于瓦片源极高轻量展示、离线瓦片
mapbox_maps_flutter原生 Platform View部分极高定制矢量地图

选型第一原则是"合规与配额”:地图底图有明确的商用授权要求,国内产品必须使用持牌厂商的底图,海外产品才考虑 Google Maps。

1.2 定位能力分层

# pubspec.yaml
dependencies:
  geolocator: ^13.0.1
  google_maps_flutter: ^2.9.0
  flutter_map: ^7.0.2
  latlong2: ^0.9.1
  permission_handler: ^11.3.1
  • geolocator:一次性定位、持续定位流、距离过滤、权限检查。
  • permission_handler:统一各平台权限申请与状态查询。
  • latlong2:经纬度计算,配合 flutter_map 使用。

1.3 精度与耗电的权衡

定位精度越高,耗电越大。系统提供从"低精度省电"到"高精度导航级"的多档精度,必须按场景选择。

精度档位误差量级耗电适用场景
lowest数公里极低城市级粗定位
low数百米低附近推荐
medium数十米中打卡签到
high十米级高导航、轨迹
best最高极高运动记录

一句话总结:先确定"需要多准”,再决定用哪档精度,否则不是定位不准就是电量崩掉。


二、地图 SDK 接入

2.1 Android 与 iOS 配置

Google Maps 需要在两端分别配置 API Key,Android 用 AndroidManifest.xml,iOS 在 AppDelegate 中设置。

<!-- android/app/src/main/AndroidManifest.xml -->
<meta-data
  android:name="com.google.android.geo.API_KEY"
  android:value="YOUR_ANDROID_MAPS_KEY" />
// ios/Runner/AppDelegate.swift
GMSServices.provideAPIKey("YOUR_IOS_MAPS_KEY")

2.2 渲染地图

class MapPage extends StatelessWidget {
  const MapPage({super.key});

  @override
  Widget build(BuildContext context) {
    return GoogleMap(
      initialCameraPosition: const CameraPosition(
        target: LatLng(39.9042, 116.4074),
        zoom: 14,
      ),
      onMapCreated: (controller) {
        // 地图就绪后才能做移动、加标记等操作
      },
      myLocationEnabled: true,
      myLocationButtonEnabled: true,
    );
  }
}

2.3 Platform View 的性能代价

地图是原生视图,在 Flutter 的渲染树中属于 Platform View,会有额外的合成开销。列表里嵌地图、地图上叠大量 Flutter Widget 都会明显掉帧。

  • 地图尽量独占一个页面,不要放进滚动列表。
  • 覆盖物优先用地图自身的 Marker / Polyline,而不是叠加 Flutter Widget。
  • 需要叠加复杂 Flutter UI 时,控制层级数量与重建频率。

一句话总结:地图是"嵌在 Flutter 里的原生窗口",把它当成一个重组件对待,性能问题会少一半。


三、定位与权限

3.1 权限申请

定位权限分"仅本次使用"与"始终允许"两档,后台定位必须申请后者。申请前应先检查服务是否开启。

Future<bool> ensurePermission() async {
  var permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
  }
  if (permission == LocationPermission.deniedForever) {
    await Geolocator.openAppSettings(); // 引导用户去设置页
    return false;
  }
  return permission == LocationPermission.whileInUse ||
      permission == LocationPermission.always;
}

3.2 一次性定位

Future<Position?> locateOnce() async {
  final serviceEnabled = await Geolocator.isLocationServiceEnabled();
  if (!serviceEnabled) return null;

  return Geolocator.getCurrentPosition(
    locationSettings: const LocationSettings(
      accuracy: LocationAccuracy.high,
      timeLimit: Duration(seconds: 15),
    ),
  );
}

3.3 持续定位与距离过滤

持续定位必须设置 distanceFilter,否则设备会以极高频率回调,耗电与流量都会失控。

final stream = Geolocator.getPositionStream(
  locationSettings: const LocationSettings(
    accuracy: LocationAccuracy.high,
    distanceFilter: 20, // 移动 20 米才回调一次
  ),
);
stream.listen((position) {
  debugPrint('当前坐标:${position.latitude}, ${position.longitude}');
});

3.4 平台权限声明

<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- ios/Runner/Info.plist -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>用于展示你附近的推荐内容</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>用于在后台记录你的运动轨迹</string>

一句话总结:定位的稳定性取决于权限档位与距离过滤,distanceFilter 不设,再好的策略也救不了耗电。


四、标记、折线与覆盖物

4.1 标记与信息窗口

final markers = shops.map((shop) {
  return Marker(
    markerId: MarkerId(shop.id),
    position: LatLng(shop.lat, shop.lng),
    infoWindow: InfoWindow(
      title: shop.name,
      snippet: shop.address,
      onTap: () => openDetail(shop.id),
    ),
  );
}).toSet();

4.2 折线与多边形

final polyline = Polyline(
  polylineId: const PolylineId('route'),
  points: trackPoints,
  color: Colors.blue,
  width: 5,
);

final polygon = Polygon(
  polygonId: const PolygonId('fence'),
  points: fencePoints,
  strokeWidth: 2,
  fillColor: Colors.blue.withOpacity(0.2),
);

4.3 大量标记的性能

当标记数量达到数百上千时,逐个 Marker 会明显卡顿,必须做聚合(clustering)或视野裁剪。

标记数量推荐策略
少于 50直接渲染
50 到 500按视野范围裁剪
500 以上网格聚合 + 缩放级别切换

一句话总结:标记的性能上限很低,超过阈值必须做聚合,否则地图一拖动就掉帧。


五、地理围栏与轨迹

5.1 地理围栏的两种实现

地理围栏有"系统级"与"应用级"两种实现。系统级由操作系统在后台监测,耗电低但精度与数量受限;应用级由应用自己判断,灵活但需要后台定位权限。

bool isInsideFence(LatLng point, LatLng center, double radiusMeters) {
  final distance = Geolocator.distanceBetween(
    point.latitude, point.longitude,
    center.latitude, center.longitude,
  );
  return distance <= radiusMeters;
}

5.2 轨迹记录

轨迹记录要在"精度"与"电量"之间取平衡:采用距离过滤 + 抽稀算法,把冗余点剔除。

  • 设置 distanceFilter 至少 10 米。
  • 对静止状态做超时判断,避免原地记录大量重复点。
  • 上传时分批打包,避免逐点请求。
  • 长时间记录需申请后台定位权限并提示用户。

5.3 坐标系统问题

国内地图存在坐标系差异:GPS 原始坐标(WGS84)、国测局加密坐标(GCJ02)、百度坐标(BD09)互不相同。混用会导致"地图上偏移几百米"。

坐标系用途说明
WGS84GPS 原始国际标准
GCJ02高德、腾讯国测局加密
BD09百度在 GCJ02 上二次偏移

一句话总结:国内做地图,坐标系转换是必修课,不转换就一定偏移。


六、性能与踩坑清单

6.1 耗电与后台限制

  • 后台定位在 iOS 上需 always 权限与后台模式声明。
  • Android 12 以后后台定位权限必须单独申请,且申请前要先有前台定位权限。
  • 长期后台定位会被系统降频,不能假设秒级更新。

6.2 常见踩坑清单

  • 未检查 isLocationServiceEnabled:用户关了 GPS 后一直转圈。
  • distanceFilter 设为 0:持续定位疯狂回调,耗电爆炸。
  • 未处理 deniedForever:用户拒绝后反复弹窗,体验极差。
  • 坐标系未转换:国内地图标记整体偏移。
  • 地图放进滚动列表:Platform View 合成开销导致掉帧。
  • 标记未聚合:上千标记直接卡死。
  • 模拟器定位固定:真机测试才能暴露定位漂移问题。

6.3 调试手段

# Android 模拟器注入模拟坐标
adb emu geo fix 116.4074 39.9042

# 查看 Android 定位相关日志
adb logcat -s GeolocatorPlugin FusedLocationProvider

一句话总结:地图与定位的问题八成来自权限、精度与坐标系,把这三件事在真机上逐一验证,能省掉大部分线上反馈。


FAQ

常见问题:为什么定位一直转圈拿不到坐标?

答:按顺序排查四件事:系统定位服务是否开启(isLocationServiceEnabled)、应用是否获得定位权限、是否处于室内或信号遮挡环境、是否设置了过短的 timeLimit 导致提前超时。前三项用代码判断,第四项可适当放宽超时。

常见问题:国内用 Google Maps 可以吗?

答:国内产品不建议使用。地图底图涉及测绘资质与合规要求,且 Google Maps 在国内服务不可用。国内应使用高德、百度、腾讯等持牌厂商的地图 SDK。

常见问题:应用切到后台后定位就停了?

答:需要申请"始终允许"定位权限,并在 iOS 的 UIBackgroundModes 中声明 location,Android 侧则要单独申请 ACCESS_BACKGROUND_LOCATION 且先取得前台定位权限。此外系统会对后台定位降频,秒级更新不可靠。

常见问题:地图上的标记和实际位置差了几百米?

答:几乎可以确定是坐标系不匹配。GPS 返回的是 WGS84,而国内地图使用 GCJ02 或 BD09,必须做坐标转换后再交给地图渲染,否则会有系统性偏移。

常见问题:地理围栏用系统 API 还是自己算?

答:围栏数量少且需要长时间后台监测时,优先用系统级地理围栏,省电且系统会唤起应用;围栏逻辑复杂、需要频繁变化时,用应用级自行计算,但必须配合后台定位权限并控制计算频率。

常见问题:怎么降低定位的耗电?

答:三招:按场景选择最低够用的精度档位;设置合理的 distanceFilter 过滤静止时的冗余回调;不需要时及时取消 getPositionStream 的订阅,避免后台持续运行。

常见问题:地图上放很多标记卡顿怎么办?

答:数量超过 50 就要考虑优化:按当前视野范围裁剪不可见标记,超过 500 时使用网格聚合,缩放级别较低时合并为聚合点,放大后再展开为独立标记。


相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Flutter」更多文章

  1. 《GraphQL 与 gRPC 客户端:类型安全 API 与代码生成》
  2. 《图表与数据可视化:fl_chart、自绘图表与实时数据展示》
  3. 《支付与内购:应用内购买、订阅与第三方支付》