开篇:地图上那个"我在哪"的小蓝点不动了
打车、外卖、社交打卡类应用几乎都离不开地图与定位。上线后最常见的反馈是:“定位图标一直转圈"“在室内定位飘到几百米外"“应用切到后台就不更新位置了”。这些问题的根源分布在三层:系统权限、定位策略、地图 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)互不相同。混用会导致"地图上偏移几百米"。
| 坐标系 | 用途 | 说明 |
|---|---|---|
| WGS84 | GPS 原始 | 国际标准 |
| 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 平台通道 — 地图原生视图与定位插件如何跨端通信
- Flutter 性能优化 — Platform View 与大量标记的帧率优化
- Flutter 权限与安全加固 — 敏感权限申请与用户数据保护
- Flutter 异步与网络 — 轨迹批量上传的网络层设计
- Flutter 本地存储 — 离线轨迹与地图缓存的持久化方案
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。