Flutter 与定位服务桥接技术详解
引言:定位服务在现代应用中的重要性
在移动应用开发中,定位服务已经成为许多应用的核心功能之一。无论是地图导航、外卖配送、社交签到,还是基于位置的推荐系统,都离不开精准的定位功能。Flutter 作为跨平台框架,提供了与原生定位服务桥接的能力,使开发者能够在 Android 和 iOS 平台上实现高效的定位功能。
本文将通过一个实际案例——开发一款名为"GeoTracker"的位置追踪应用——来详细介绍 Flutter 中实现定位服务的技术细节和最佳实践。
定位服务技术概述
定位技术类型
- GPS 定位:通过卫星信号提供高精度定位
- 网络定位:通过 WiFi 和基站信号提供定位
- 蓝牙定位:通过蓝牙信标进行室内定位
- 混合定位:结合多种定位技术提高精度
定位精度与功耗权衡
- 高精度模式:GPS+网络,精度高但功耗大
- 平衡模式:平衡精度和功耗
- 低功耗模式:主要使用网络定位,功耗低但精度有限
项目背景:GeoTracker 位置追踪应用
我们的项目是开发一款名为 GeoTracker 的位置追踪应用,支持以下功能:
- 实时位置追踪
- 历史轨迹记录
- 地理围栏监控
- 位置数据可视化
- 离线位置缓存
技术架构设计
整体架构
┌─────────────────────────────────────────────────────────────┐
│ Flutter应用层 │
├─────────────────────────────────────────────────────────────┤
│ 地图UI │ 轨迹UI │ 设置页面 │ 统计页面 │
├─────────────────────────────────────────────────────────────┤
│ 定位服务管理层 │
├─────────────────────────────────────────────────────────────┤
│ 平台通道桥接层 │
├─────────────────────────────────────────────────────────────┤
│ Android LocationManager │ iOS CoreLocation │
└─────────────────────────────────────────────────────────────┘核心组件
- LocationService:定位服务管理
- GeofenceManager:地理围栏管理
- LocationDatabase:位置数据存储
- LocationAnalytics:位置数据分析
- PlatformChannel:平台通道通信
实现步骤详解
第一步:添加依赖和配置
首先,我们需要添加必要的依赖包:
yaml
dependencies:
flutter:
sdk: flutter
geolocator: ^10.1.0
permission_handler: ^10.2.0
google_maps_flutter: ^2.5.0
polyline_points: ^2.0.0
shared_preferences: ^2.2.0
sqflite: ^2.3.0
path_provider: ^2.1.0Android 平台需要配置权限和特性:
xml
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 定位权限 -->
<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" />
<!-- 后台定位权限(Android 10+) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application>
<!-- 位置服务配置 -->
<service
android:name=".LocationService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" />
</application>
</manifest>iOS 平台需要在 Info.plist 中添加定位权限说明:
xml
<!-- ios/Runner/Info.plist -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>此应用需要访问您的位置来提供位置追踪服务</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>此应用需要后台访问您的位置来提供持续的位置追踪服务</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>此应用需要后台访问您的位置来提供持续的位置追踪服务</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>background-fetch</string>
</array>第二步:创建定位服务管理器
dart
// lib/services/location_service.dart
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LocationService {
static final LocationService _instance = LocationService._internal();
factory LocationService() => _instance;
LocationService._internal();
final StreamController<Position> _positionStreamController = StreamController<Position>.broadcast();
final StreamController<LocationStatus> _statusStreamController = StreamController<LocationStatus>.broadcast();
StreamSubscription<Position>? _positionSubscription;
LocationSettings? _locationSettings;
bool _isTracking = false;
LocationAccuracy _desiredAccuracy = LocationAccuracy.high;
int _interval = 5000; // 5秒
double _distanceFilter = 10.0; // 10米
// 位置更新流
Stream<Position> get positionStream => _positionStreamController.stream;
// 定位状态流
Stream<LocationStatus> get statusStream => _statusStreamController.stream;
// 当前是否正在追踪
bool get isTracking => _isTracking;
// 当前定位设置
LocationSettings? get locationSettings => _locationSettings;
// 初始化定位服务
Future<void> initialize() async {
try {
// 检查定位服务是否可用
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
throw LocationServiceException('定位服务未启用');
}
// 检查权限
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
throw LocationServiceException('定位权限被拒绝');
}
}
if (permission == LocationPermission.deniedForever) {
throw LocationServiceException('定位权限被永久拒绝,请在设置中开启');
}
// 加载保存的设置
await _loadSettings();
// 配置定位设置
_configureLocationSettings();
} catch (e) {
throw LocationServiceException('定位服务初始化失败: $e');
}
}
// 配置定位设置
void _configureLocationSettings() {
if (Platform.isAndroid) {
_locationSettings = AndroidSettings(
accuracy: _desiredAccuracy,
distanceFilter: _distanceFilter,
intervalDuration: Duration(milliseconds: _interval),
foregroundNotificationConfig: const ForegroundNotificationConfig(
notificationText: "GeoTracker正在后台追踪您的位置",
notificationTitle: "位置追踪服务",
enableWakeLock: true,
),
);
} else if (Platform.isIOS) {
_locationSettings = AppleSettings(
accuracy: _desiredAccuracy,
activityType: ActivityType.automotiveNavigation,
distanceFilter: _distanceFilter,
pauseLocationUpdatesAutomatically: true,
showBackgroundLocationIndicator: true,
allowBackgroundLocationUpdates: true,
);
}
}
// 加载保存的设置
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
_desiredAccuracy = LocationAccuracy.values[prefs.getInt('accuracy') ?? 2];
_interval = prefs.getInt('interval') ?? 5000;
_distanceFilter = prefs.getDouble('distanceFilter') ?? 10.0;
}
// 保存设置
Future<void> _saveSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('accuracy', _desiredAccuracy.index);
await prefs.setInt('interval', _interval);
await prefs.setDouble('distanceFilter', _distanceFilter);
}
// 开始位置追踪
Future<void> startTracking() async {
if (_isTracking) return;
try {
// 确保已初始化
if (_locationSettings == null) {
await initialize();
}
// 获取当前位置
final Position initialPosition = await Geolocator.getCurrentPosition(
desiredAccuracy: _desiredAccuracy,
timeLimit: const Duration(seconds: 10),
);
// 发送初始位置
_positionStreamController.add(initialPosition);
// 开始位置更新
_positionSubscription = Geolocator.getPositionStream(
locationSettings: _locationSettings!,
).listen(
(Position position) {
_positionStreamController.add(position);
_statusStreamController.add(LocationStatus.tracking);
},
onError: (error) {
_statusStreamController.add(LocationStatus.error);
debugPrint('位置追踪错误: $error');
},
);
_isTracking = true;
_statusStreamController.add(LocationStatus.tracking);
} catch (e) {
_statusStreamController.add(LocationStatus.error);
throw LocationServiceException('开始位置追踪失败: $e');
}
}
// 停止位置追踪
Future<void> stopTracking() async {
if (!_isTracking) return;
try {
await _positionSubscription?.cancel();
_positionSubscription = null;
_isTracking = false;
_statusStreamController.add(LocationStatus.stopped);
} catch (e) {
throw LocationServiceException('停止位置追踪失败: $e');
}
}
// 获取当前位置
Future<Position> getCurrentPosition({
LocationAccuracy? desiredAccuracy,
Duration? timeLimit,
}) async {
try {
return await Geolocator.getCurrentPosition(
desiredAccuracy: desiredAccuracy ?? _desiredAccuracy,
timeLimit: timeLimit ?? const Duration(seconds: 10),
);
} catch (e) {
throw LocationServiceException('获取当前位置失败: $e');
}
}
// 计算两点间距离
double calculateDistance(
double startLatitude,
double startLongitude,
double endLatitude,
double endLongitude,
) {
return Geolocator.distanceBetween(
startLatitude,
startLongitude,
endLatitude,
endLongitude,
);
}
// 更新定位设置
Future<void> updateSettings({
LocationAccuracy? accuracy,
int? interval,
double? distanceFilter,
}) async {
// 停止当前追踪
if (_isTracking) {
await stopTracking();
}
// 更新设置
if (accuracy != null) _desiredAccuracy = accuracy;
if (interval != null) _interval = interval;
if (distanceFilter != null) _distanceFilter = distanceFilter;
// 保存设置
await _saveSettings();
// 重新配置定位设置
_configureLocationSettings();
// 如果之前在追踪,重新开始
if (_isTracking) {
await startTracking();
}
}
// 检查定位服务状态
Future<bool> isLocationServiceEnabled() async {
return await Geolocator.isLocationServiceEnabled();
}
// 打开定位设置
Future<bool> openLocationSettings() async {
return await Geolocator.openLocationSettings();
}
// 打开应用设置
Future<bool> openAppSettings() async {
return await Geolocator.openAppSettings();
}
// 释放资源
Future<void> dispose() async {
await stopTracking();
await _positionStreamController.close();
await _statusStreamController.close();
}
}
// 定位状态枚举
enum LocationStatus {
initialized,
tracking,
stopped,
error,
permissionDenied,
serviceDisabled,
}
// 定位服务异常
class LocationServiceException implements Exception {
final String message;
LocationServiceException(this.message);
@override
String toString() => message;
}第三步:实现地理围栏管理
dart
// lib/services/geofence_manager.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:collection/collection.dart';
class GeofenceManager {
static final GeofenceManager _instance = GeofenceManager._internal();
factory GeofenceManager() => _instance;
GeofenceManager._internal();
final List<GeofenceRegion> _geofences = [];
final StreamController<GeofenceEvent> _eventStreamController = StreamController<GeofenceEvent>.broadcast();
StreamSubscription<Position>? _positionSubscription;
// 地理围栏事件流
Stream<GeofenceEvent> get eventStream => _eventStreamController.stream;
// 当前地理围栏列表
List<GeofenceRegion> get geofences => List.unmodifiable(_geofences);
// 初始化地理围栏管理器
Future<void> initialize() async {
// 加载保存的地理围栏
await _loadGeofences();
// 开始监听位置变化
_startLocationMonitoring();
}
// 添加地理围栏
Future<void> addGeofence(GeofenceRegion geofence) async {
if (_geofences.any((g) => g.id == geofence.id)) {
throw GeofenceException('地理围栏ID已存在: ${geofence.id}');
}
_geofences.add(geofence);
await _saveGeofences();
}
// 移除地理围栏
Future<void> removeGeofence(String geofenceId) async {
_geofences.removeWhere((g) => g.id == geofenceId);
await _saveGeofences();
}
// 更新地理围栏
Future<void> updateGeofence(GeofenceRegion geofence) async {
final index = _geofences.indexWhere((g) => g.id == geofence.id);
if (index == -1) {
throw GeofenceException('地理围栏不存在: ${geofence.id}');
}
_geofences[index] = geofence;
await _saveGeofences();
}
// 获取地理围栏
GeofenceRegion? getGeofence(String geofenceId) {
try {
return _geofences.firstWhere((g) => g.id == geofenceId);
} catch (e) {
return null;
}
}
// 开始位置监控
void _startLocationMonitoring() {
_positionSubscription = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 10, // 10米
),
).listen(_onPositionChanged);
}
// 处理位置变化
void _onPositionChanged(Position position) {
for (final geofence in _geofences) {
final distance = Geolocator.distanceBetween(
position.latitude,
position.longitude,
geofence.latitude,
geofence.longitude,
);
final isInside = distance <= geofence.radius;
final wasInside = geofence.isInside;
// 状态变化时触发事件
if (isInside != wasInside) {
geofence.isInside = isInside;
final event = GeofenceEvent(
geofenceId: geofence.id,
geofenceName: geofence.name,
eventType: isInside ? GeofenceEventType.enter : GeofenceEventType.exit,
position: position,
timestamp: DateTime.now(),
);
_eventStreamController.add(event);
// 执行回调
if (isInside && geofence.onEnter != null) {
geofence.onEnter!(event);
} else if (!isInside && geofence.onExit != null) {
geofence.onExit!(event);
}
}
}
}
// 检查位置是否在地理围栏内
List<GeofenceRegion> getGeofencesAtPosition(Position position) {
return _geofences.where((geofence) {
final distance = Geolocator.distanceBetween(
position.latitude,
position.longitude,
geofence.latitude,
geofence.longitude,
);
return distance <= geofence.radius;
}).toList();
}
// 加载地理围栏
Future<void> _loadGeofences() async {
try {
final prefs = await SharedPreferences.getInstance();
final geofencesJson = prefs.getStringList('geofences') ?? [];
_geofences.clear();
for (final json in geofencesJson) {
final Map<String, dynamic> data = jsonDecode(json);
_geofences.add(GeofenceRegion.fromJson(data));
}
} catch (e) {
debugPrint('加载地理围栏失败: $e');
}
}
// 保存地理围栏
Future<void> _saveGeofences() async {
try {
final prefs = await SharedPreferences.getInstance();
final geofencesJson = _geofences.map((g) => jsonEncode(g.toJson())).toList();
await prefs.setStringList('geofences', geofencesJson);
} catch (e) {
debugPrint('保存地理围栏失败: $e');
}
}
// 清除所有地理围栏
Future<void> clearAllGeofences() async {
_geofences.clear();
await _saveGeofences();
}
// 释放资源
Future<void> dispose() async {
await _positionSubscription?.cancel();
await _eventStreamController.close();
}
}
// 地理围栏区域
class GeofenceRegion {
final String id;
final String name;
final double latitude;
final double longitude;
final double radius;
final GeofenceTransitionType transitionType;
final Function(GeofenceEvent)? onEnter;
final Function(GeofenceEvent)? onExit;
bool isInside;
GeofenceRegion({
required this.id,
required this.name,
required this.latitude,
required this.longitude,
required this.radius,
this.transitionType = GeofenceTransitionType.both,
this.onEnter,
this.onExit,
this.isInside = false,
});
factory GeofenceRegion.fromJson(Map<String, dynamic> json) {
return GeofenceRegion(
id: json['id'],
name: json['name'],
latitude: json['latitude'].toDouble(),
longitude: json['longitude'].toDouble(),
radius: json['radius'].toDouble(),
transitionType: GeofenceTransitionType.values[json['transitionType']],
isInside: json['isInside'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'latitude': latitude,
'longitude': longitude,
'radius': radius,
'transitionType': transitionType.index,
'isInside': isInside,
};
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is GeofenceRegion && other.id == id;
}
@override
int get hashCode => id.hashCode;
}
// 地理围栏事件
class GeofenceEvent {
final String geofenceId;
final String geofenceName;
final GeofenceEventType eventType;
final Position position;
final DateTime timestamp;
GeofenceEvent({
required this.geofenceId,
required this.geofenceName,
required this.eventType,
required this.position,
required this.timestamp,
});
Map<String, dynamic> toJson() {
return {
'geofenceId': geofenceId,
'geofenceName': geofenceName,
'eventType': eventType.index,
'latitude': position.latitude,
'longitude': position.longitude,
'timestamp': timestamp.toIso8601String(),
};
}
}
// 地理围栏事件类型
enum GeofenceEventType {
enter,
exit,
}
// 地理围栏转换类型
enum GeofenceTransitionType {
enter,
exit,
both,
}
// 地理围栏异常
class GeofenceException implements Exception {
final String message;
GeofenceException(this.message);
@override
String toString() => message;
}第四步:实现位置数据存储
dart
// lib/services/location_database.dart
import 'dart:async';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:geolocator/geolocator.dart';
class LocationDatabase {
static final LocationDatabase _instance = LocationDatabase._internal();
factory LocationDatabase() => _instance;
LocationDatabase._internal();
Database? _database;
// 获取数据库实例
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
// 初始化数据库
Future<Database> _initDatabase() async {
final databasesPath = await getDatabasesPath();
final path = join(databasesPath, 'location_tracker.db');
return await openDatabase(
path,
version: 1,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
// 创建数据库表
Future<void> _onCreate(Database db, int version) async {
// 位置记录表
await db.execute('''
CREATE TABLE locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
latitude REAL NOT NULL,
longitude REAL NOT NULL,
altitude REAL,
accuracy REAL,
speed REAL,
heading REAL,
timestamp INTEGER NOT NULL,
is_mocked INTEGER DEFAULT 0
)
''');
// 轨迹表
await db.execute('''
CREATE TABLE tracks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
start_time INTEGER NOT NULL,
end_time INTEGER,
distance REAL DEFAULT 0,
created_at INTEGER NOT NULL
)
''');
// 轨迹点关联表
await db.execute('''
CREATE TABLE track_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
track_id INTEGER NOT NULL,
location_id INTEGER NOT NULL,
order_index INTEGER NOT NULL,
FOREIGN KEY (track_id) REFERENCES tracks (id) ON DELETE CASCADE,
FOREIGN KEY (location_id) REFERENCES locations (id) ON DELETE CASCADE
)
''');
// 地理围栏事件表
await db.execute('''
CREATE TABLE geofence_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
geofence_id TEXT NOT NULL,
geofence_name TEXT NOT NULL,
event_type INTEGER NOT NULL,
latitude REAL NOT NULL,
longitude REAL NOT NULL,
timestamp INTEGER NOT NULL
)
''');
// 创建索引
await db.execute('CREATE INDEX idx_locations_timestamp ON locations(timestamp)');
await db.execute('CREATE INDEX idx_tracks_start_time ON tracks(start_time)');
await db.execute('CREATE INDEX idx_track_points_track_id ON track_points(track_id)');
await db.execute('CREATE INDEX idx_geofence_events_timestamp ON geofence_events(timestamp)');
}
// 数据库升级
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
// 根据版本进行升级
if (oldVersion < 2) {
// 添加新字段或表
}
}
// 保存位置记录
Future<int> saveLocation(Position position) async {
final db = await database;
return await db.insert('locations', {
'latitude': position.latitude,
'longitude': position.longitude,
'altitude': position.altitude,
'accuracy': position.accuracy,
'speed': position.speed,
'heading': position.heading,
'timestamp': position.timestamp?.millisecondsSinceEpoch ?? DateTime.now().millisecondsSinceEpoch,
'is_mocked': position.isMocked ? 1 : 0,
});
}
// 获取位置记录
Future<List<LocationRecord>> getLocations({
DateTime? startTime,
DateTime? endTime,
int? limit,
int? offset,
}) async {
final db = await database;
String whereClause = '';
List<dynamic> whereArgs = [];
if (startTime != null) {
whereClause += 'timestamp >= ?';
whereArgs.add(startTime.millisecondsSinceEpoch);
}
if (endTime != null) {
if (whereClause.isNotEmpty) whereClause += ' AND ';
whereClause += 'timestamp <= ?';
whereArgs.add(endTime.millisecondsSinceEpoch);
}
final List<Map<String, dynamic>> maps = await db.query(
'locations',
where: whereClause.isNotEmpty ? whereClause : null,
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
orderBy: 'timestamp DESC',
limit: limit,
offset: offset,
);
return List.generate(maps.length, (i) {
return LocationRecord.fromMap(maps[i]);
});
}
// 创建轨迹
Future<int> createTrack(String name, {String? description}) async {
final db = await database;
return await db.insert('tracks', {
'name': name,
'description': description,
'start_time': DateTime.now().millisecondsSinceEpoch,
'created_at': DateTime.now().millisecondsSinceEpoch,
});
}
// 添加轨迹点
Future<void> addTrackPoint(int trackId, int locationId, int orderIndex) async {
final db = await database;
await db.insert('track_points', {
'track_id': trackId,
'location_id': locationId,
'order_index': orderIndex,
});
}
// 完成轨迹
Future<void> completeTrack(int trackId, double distance) async {
final db = await database;
await db.update(
'tracks',
{
'end_time': DateTime.now().millisecondsSinceEpoch,
'distance': distance,
},
where: 'id = ?',
whereArgs: [trackId],
);
}
// 获取轨迹列表
Future<List<Track>> getTracks() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(
'tracks',
orderBy: 'start_time DESC',
);
return List.generate(maps.length, (i) {
return Track.fromMap(maps[i]);
});
}
// 获取轨迹详情
Future<TrackDetail?> getTrackDetail(int trackId) async {
final db = await database;
// 获取轨迹信息
final List<Map<String, dynamic>> trackMaps = await db.query(
'tracks',
where: 'id = ?',
whereArgs: [trackId],
);
if (trackMaps.isEmpty) return null;
final track = Track.fromMap(trackMaps.first);
// 获取轨迹点
final List<Map<String, dynamic>> pointMaps = await db.rawQuery('''
SELECT l.* FROM locations l
INNER JOIN track_points tp ON l.id = tp.location_id
WHERE tp.track_id = ?
ORDER BY tp.order_index
''', [trackId]);
final points = pointMaps.map((map) => LocationRecord.fromMap(map)).toList();
return TrackDetail(track: track, points: points);
}
// 保存地理围栏事件
Future<int> saveGeofenceEvent(GeofenceEventRecord event) async {
final db = await database;
return await db.insert('geofence_events', {
'geofence_id': event.geofenceId,
'geofence_name': event.geofenceName,
'event_type': event.eventType.index,
'latitude': event.latitude,
'longitude': event.longitude,
'timestamp': event.timestamp.millisecondsSinceEpoch,
});
}
// 获取地理围栏事件
Future<List<GeofenceEventRecord>> getGeofenceEvents({
String? geofenceId,
DateTime? startTime,
DateTime? endTime,
int? limit,
}) async {
final db = await database;
String whereClause = '';
List<dynamic> whereArgs = [];
if (geofenceId != null) {
whereClause += 'geofence_id = ?';
whereArgs.add(geofenceId);
}
if (startTime != null) {
if (whereClause.isNotEmpty) whereClause += ' AND ';
whereClause += 'timestamp >= ?';
whereArgs.add(startTime.millisecondsSinceEpoch);
}
if (endTime != null) {
if (whereClause.isNotEmpty) whereClause += ' AND ';
whereClause += 'timestamp <= ?';
whereArgs.add(endTime.millisecondsSinceEpoch);
}
final List<Map<String, dynamic>> maps = await db.query(
'geofence_events',
where: whereClause.isNotEmpty ? whereClause : null,
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
orderBy: 'timestamp DESC',
limit: limit,
);
return List.generate(maps.length, (i) {
return GeofenceEventRecord.fromMap(maps[i]);
});
}
// 清理旧数据
Future<void> cleanupOldData({int daysToKeep = 30}) async {
final db = await database;
final cutoffTime = DateTime.now().subtract(Duration(days: daysToKeep)).millisecondsSinceEpoch;
// 删除旧的位置记录
await db.delete('locations', where: 'timestamp < ?', whereArgs: [cutoffTime]);
// 删除旧的地理围栏事件
await db.delete('geofence_events', where: 'timestamp < ?', whereArgs: [cutoffTime]);
}
// 获取数据库统计信息
Future<Map<String, int>> getDatabaseStats() async {
final db = await database;
final locationCount = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM locations')
) ?? 0;
final trackCount = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM tracks')
) ?? 0;
final eventCount = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM geofence_events')
) ?? 0;
return {
'locations': locationCount,
'tracks': trackCount,
'events': eventCount,
};
}
// 关闭数据库
Future<void> close() async {
final db = _database;
if (db != null) {
await db.close();
_database = null;
}
}
}
// 位置记录模型
class LocationRecord {
final int id;
final double latitude;
final double longitude;
final double? altitude;
final double? accuracy;
final double? speed;
final double? heading;
final DateTime timestamp;
final bool isMocked;
LocationRecord({
required this.id,
required this.latitude,
required this.longitude,
this.altitude,
this.accuracy,
this.speed,
this.heading,
required this.timestamp,
required this.isMocked,
});
factory LocationRecord.fromMap(Map<String, dynamic> map) {
return LocationRecord(
id: map['id'],
latitude: map['latitude'].toDouble(),
longitude: map['longitude'].toDouble(),
altitude: map['altitude']?.toDouble(),
accuracy: map['accuracy']?.toDouble(),
speed: map['speed']?.toDouble(),
heading: map['heading']?.toDouble(),
timestamp: DateTime.fromMillisecondsSinceEpoch(map['timestamp']),
isMocked: map['is_mocked'] == 1,
);
}
Position toPosition() {
return Position(
latitude: latitude,
longitude: longitude,
timestamp: timestamp,
accuracy: accuracy ?? 0,
altitude: altitude,
heading: heading,
speed: speed,
isMocked: isMocked,
);
}
}
// 轨迹模型
class Track {
final int id;
final String name;
final String? description;
final DateTime startTime;
final DateTime? endTime;
final double distance;
final DateTime createdAt;
Track({
required this.id,
required this.name,
this.description,
required this.startTime,
this.endTime,
required this.distance,
required this.createdAt,
});
factory Track.fromMap(Map<String, dynamic> map) {
return Track(
id: map['id'],
name: map['name'],
description: map['description'],
startTime: DateTime.fromMillisecondsSinceEpoch(map['start_time']),
endTime: map['end_time'] != null
? DateTime.fromMillisecondsSinceEpoch(map['end_time'])
: null,
distance: map['distance'].toDouble(),
createdAt: DateTime.fromMillisecondsSinceEpoch(map['created_at']),
);
}
Duration? get duration {
if (endTime == null) return null;
return endTime!.difference(startTime);
}
}
// 轨迹详情模型
class TrackDetail {
final Track track;
final List<LocationRecord> points;
TrackDetail({
required this.track,
required this.points,
});
}
// 地理围栏事件记录模型
class GeofenceEventRecord {
final int id;
final String geofenceId;
final String geofenceName;
final GeofenceEventType eventType;
final double latitude;
final double longitude;
final DateTime timestamp;
GeofenceEventRecord({
required this.id,
required this.geofenceId,
required this.geofenceName,
required this.eventType,
required this.latitude,
required this.longitude,
required this.timestamp,
});
factory GeofenceEventRecord.fromMap(Map<String, dynamic> map) {
return GeofenceEventRecord(
id: map['id'],
geofenceId: map['geofence_id'],
geofenceName: map['geofence_name'],
eventType: GeofenceEventType.values[map['event_type']],
latitude: map['latitude'].toDouble(),
longitude: map['longitude'].toDouble(),
timestamp: DateTime.fromMillisecondsSinceEpoch(map['timestamp']),
);
}
}第五步:创建位置追踪 UI 组件
dart
// lib/widgets/location_tracker_widget.dart
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import '../services/location_service.dart';
import '../services/location_database.dart';
import '../services/geofence_manager.dart';
class LocationTrackerWidget extends StatefulWidget {
const LocationTrackerWidget({Key? key}) : super(key: key);
@override
_LocationTrackerWidgetState createState() => _LocationTrackerWidgetState();
}
class _LocationTrackerWidgetState extends State<LocationTrackerWidget> {
final LocationService _locationService = LocationService();
final GeofenceManager _geofenceManager = GeofenceManager();
final LocationDatabase _database = LocationDatabase();
GoogleMapController? _mapController;
Set<Marker> _markers = {};
Set<Polyline> _polylines = {};
Set<Circle> _circles = {};
Position? _currentPosition;
List<LocationRecord> _locationHistory = [];
int? _currentTrackId;
double _totalDistance = 0.0;
bool _isTracking = false;
bool _isLoading = false;
@override
void initState() {
super.initState();
_initializeServices();
}
@override
void dispose() {
_locationService.dispose();
_geofenceManager.dispose();
super.dispose();
}
Future<void> _initializeServices() async {
setState(() => _isLoading = true);
try {
// 初始化服务
await _locationService.initialize();
await _geofenceManager.initialize();
// 监听位置更新
_locationService.positionStream.listen(_onLocationUpdate);
// 监听地理围栏事件
_geofenceManager.eventStream.listen(_onGeofenceEvent);
// 加载最近的位置历史
await _loadLocationHistory();
// 获取当前位置
_currentPosition = await _locationService.getCurrentPosition();
_updateMap();
} catch (e) {
_showErrorSnackBar('初始化失败: $e');
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _loadLocationHistory() async {
final locations = await _database.getLocations(limit: 100);
setState(() {
_locationHistory = locations;
_updatePolylines();
});
}
void _onLocationUpdate(Position position) async {
_currentPosition = position;
if (_isTracking) {
// 保存位置记录
final locationId = await _database.saveLocation(position);
// 添加到轨迹
if (_currentTrackId != null) {
final orderIndex = _locationHistory.length;
await _database.addTrackPoint(_currentTrackId!, locationId, orderIndex);
// 计算距离
if (_locationHistory.isNotEmpty) {
final lastLocation = _locationHistory.last;
final distance = _locationService.calculateDistance(
lastLocation.latitude,
lastLocation.longitude,
position.latitude,
position.longitude,
);
_totalDistance += distance;
}
}
// 更新位置历史
setState(() {
_locationHistory.add(LocationRecord(
id: locationId,
latitude: position.latitude,
longitude: position.longitude,
altitude: position.altitude,
accuracy: position.accuracy,
speed: position.speed,
heading: position.heading,
timestamp: position.timestamp ?? DateTime.now(),
isMocked: position.isMocked,
));
_updateMap();
});
}
}
void _onGeofenceEvent(GeofenceEvent event) {
// 保存地理围栏事件
final eventRecord = GeofenceEventRecord(
id: 0, // 数据库会自动生成
geofenceId: event.geofenceId,
geofenceName: event.geofenceName,
eventType: event.eventType,
latitude: event.position.latitude,
longitude: event.position.longitude,
timestamp: event.timestamp,
);
_database.saveGeofenceEvent(eventRecord);
// 显示通知
_showGeofenceNotification(event);
}
void _showGeofenceNotification(GeofenceEvent event) {
final message = event.eventType == GeofenceEventType.enter
? '进入 ${event.geofenceName}'
: '离开 ${event.geofenceName}';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
),
);
}
void _updateMap() {
if (_currentPosition == null) return;
// 更新当前位置标记
_markers.removeWhere((marker) => marker.markerId.value == 'current');
_markers.add(Marker(
markerId: const MarkerId('current'),
position: LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
infoWindow: const InfoWindow(title: '当前位置'),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue),
));
// 更新地理围栏圆圈
_updateGeofenceCircles();
// 移动地图到当前位置
_mapController?.animateCamera(
CameraUpdate.newLatLngZoom(
LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
15.0,
),
);
setState(() {});
}
void _updatePolylines() {
if (_locationHistory.length < 2) return;
final points = _locationHistory.map((location) {
return LatLng(location.latitude, location.longitude);
}).toList();
_polylines.clear();
_polylines.add(Polyline(
polylineId: const PolylineId('track'),
points: points,
color: Colors.blue,
width: 4,
));
}
void _updateGeofenceCircles() {
_circles.clear();
for (final geofence in _geofenceManager.geofences) {
_circles.add(Circle(
circleId: CircleId(geofence.id),
center: LatLng(geofence.latitude, geofence.longitude),
radius: geofence.radius,
fillColor: geofence.isInside
? Colors.green.withOpacity(0.3)
: Colors.red.withOpacity(0.3),
strokeColor: geofence.isInside ? Colors.green : Colors.red,
strokeWidth: 2,
));
}
}
Future<void> _toggleTracking() async {
if (_isTracking) {
await _stopTracking();
} else {
await _startTracking();
}
}
Future<void> _startTracking() async {
try {
// 创建新轨迹
_currentTrackId = await _database.createTrack(
'轨迹 ${DateTime.now().toString().substring(0, 19)}',
);
// 开始位置追踪
await _locationService.startTracking();
setState(() {
_isTracking = true;
_totalDistance = 0.0;
});
_showSuccessSnackBar('开始位置追踪');
} catch (e) {
_showErrorSnackBar('开始追踪失败: $e');
}
}
Future<void> _stopTracking() async {
try {
// 停止位置追踪
await _locationService.stopTracking();
// 完成轨迹
if (_currentTrackId != null) {
await _database.completeTrack(_currentTrackId!, _totalDistance);
_currentTrackId = null;
}
setState(() {
_isTracking = false;
});
_showSuccessSnackBar('停止位置追踪');
} catch (e) {
_showErrorSnackBar('停止追踪失败: $e');
}
}
void _showGeofenceDialog() {
showDialog(
context: context,
builder: (context) => GeofenceDialog(
onGeofenceAdded: (geofence) async {
try {
await _geofenceManager.addGeofence(geofence);
_updateGeofenceCircles();
_showSuccessSnackBar('地理围栏添加成功');
} catch (e) {
_showErrorSnackBar('添加地理围栏失败: $e');
}
},
),
);
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
}
void _showSuccessSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('GeoTracker'),
backgroundColor: Colors.blue,
actions: [
IconButton(
icon: const Icon(Icons.fence),
onPressed: _showGeofenceDialog,
tooltip: '添加地理围栏',
),
IconButton(
icon: const Icon(Icons.history),
onPressed: () => _showTracksScreen(),
tooltip: '查看轨迹',
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: Stack(
children: [
// 地图
GoogleMap(
onMapCreated: (controller) => _mapController = controller,
initialCameraPosition: CameraPosition(
target: _currentPosition != null
? LatLng(_currentPosition!.latitude, _currentPosition!.longitude)
: const LatLng(39.9042, 116.4074), // 默认北京
zoom: 15.0,
),
markers: _markers,
polylines: _polylines,
circles: _circles,
myLocationEnabled: true,
myLocationButtonEnabled: false,
),
// 信息面板
Positioned(
top: 16,
left: 16,
right: 16,
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'追踪状态: ${_isTracking ? '进行中' : '已停止'}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
if (_isTracking) ...[
Text('总距离: ${_totalDistance.toStringAsFixed(2)} 米'),
Text('位置点数: ${_locationHistory.length}'),
],
if (_currentPosition != null) ...[
Text(
'当前位置: ${_currentPosition!.latitude.toStringAsFixed(6)}, ${_currentPosition!.longitude.toStringAsFixed(6)}',
),
Text('精度: ${_currentPosition?.accuracy.toStringAsFixed(2)} 米'),
],
],
),
),
),
),
// 控制按钮
Positioned(
bottom: 16,
right: 16,
child: Column(
children: [
// 返回当前位置按钮
FloatingActionButton(
heroTag: "current_location",
onPressed: () {
if (_currentPosition != null) {
_mapController?.animateCamera(
CameraUpdate.newLatLngZoom(
LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
15.0,
),
);
}
},
child: const Icon(Icons.my_location),
),
const SizedBox(height: 16),
// 开始/停止追踪按钮
FloatingActionButton.extended(
heroTag: "toggle_tracking",
onPressed: _toggleTracking,
icon: Icon(_isTracking ? Icons.stop : Icons.play_arrow),
label: Text(_isTracking ? '停止' : '开始'),
backgroundColor: _isTracking ? Colors.red : Colors.green,
),
],
),
),
],
),
);
}
void _showTracksScreen() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const TracksScreen(),
),
);
}
}
// 地理围栏对话框
class GeofenceDialog extends StatefulWidget {
final Function(GeofenceRegion) onGeofenceAdded;
const GeofenceDialog({
Key? key,
required this.onGeofenceAdded,
}) : super(key: key);
@override
_GeofenceDialogState createState() => _GeofenceDialogState();
}
class _GeofenceDialogState extends State<GeofenceDialog> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _latitudeController = TextEditingController();
final _longitudeController = TextEditingController();
final _radiusController = TextEditingController(text: '100');
@override
void dispose() {
_nameController.dispose();
_latitudeController.dispose();
_longitudeController.dispose();
_radiusController.dispose();
super.dispose();
}
void _addGeofence() {
if (_formKey.currentState!.validate()) {
final geofence = GeofenceRegion(
id: DateTime.now().millisecondsSinceEpoch.toString(),
name: _nameController.text,
latitude: double.parse(_latitudeController.text),
longitude: double.parse(_longitudeController.text),
radius: double.parse(_radiusController.text),
);
widget.onGeofenceAdded(geofence);
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('添加地理围栏'),
content: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: '名称'),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入名称';
}
return null;
},
),
TextFormField(
controller: _latitudeController,
decoration: const InputDecoration(labelText: '纬度'),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入纬度';
}
final lat = double.tryParse(value);
if (lat == null || lat < -90 || lat > 90) {
return '请输入有效的纬度值(-90到90)';
}
return null;
},
),
TextFormField(
controller: _longitudeController,
decoration: const InputDecoration(labelText: '经度'),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入经度';
}
final lng = double.tryParse(value);
if (lng == null || lng < -180 || lng > 180) {
return '请输入有效的经度值(-180到180)';
}
return null;
},
),
TextFormField(
controller: _radiusController,
decoration: const InputDecoration(labelText: '半径(米)'),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入半径';
}
final radius = double.tryParse(value);
if (radius == null || radius <= 0) {
return '请输入有效的半径值';
}
return null;
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: _addGeofence,
child: const Text('添加'),
),
],
);
}
}
// 轨迹列表页面
class TracksScreen extends StatefulWidget {
const TracksScreen({Key? key}) : super(key: key);
@override
_TracksScreenState createState() => _TracksScreenState();
}
class _TracksScreenState extends State<TracksScreen> {
final LocationDatabase _database = LocationDatabase();
List<Track> _tracks = [];
bool _isLoading = false;
@override
void initState() {
super.initState();
_loadTracks();
}
Future<void> _loadTracks() async {
setState(() => _isLoading = true);
try {
final tracks = await _database.getTracks();
setState(() {
_tracks = tracks;
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('加载轨迹失败: $e')),
);
} finally {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('轨迹历史'),
backgroundColor: Colors.blue,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _tracks.isEmpty
? const Center(
child: Text('暂无轨迹记录'),
)
: ListView.builder(
itemCount: _tracks.length,
itemBuilder: (context, index) {
final track = _tracks[index];
return TrackCard(
track: track,
onTap: () => _showTrackDetail(track),
);
},
),
);
}
void _showTrackDetail(Track track) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => TrackDetailScreen(trackId: track.id),
),
);
}
}
// 轨迹卡片组件
class TrackCard extends StatelessWidget {
final Track track;
final VoidCallback onTap;
const TrackCard({
Key? key,
required this.track,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
title: Text(track.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('开始时间: ${_formatDateTime(track.startTime)}'),
if (track.endTime != null)
Text('结束时间: ${_formatDateTime(track.endTime!)}'),
Text('距离: ${track.distance.toStringAsFixed(2)} 米'),
if (track.duration != null)
Text('时长: ${_formatDuration(track.duration!)}'),
],
),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: onTap,
),
);
}
String _formatDateTime(DateTime dateTime) {
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}
String _formatDuration(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
if (hours > 0) {
return '${hours}小时${minutes}分钟${seconds}秒';
} else if (minutes > 0) {
return '${minutes}分钟${seconds}秒';
} else {
return '${seconds}秒';
}
}
}
// 轨迹详情页面
class TrackDetailScreen extends StatefulWidget {
final int trackId;
const TrackDetailScreen({
Key? key,
required this.trackId,
}) : super(key: key);
@override
_TrackDetailScreenState createState() => _TrackDetailScreenState();
}
class _TrackDetailScreenState extends State<TrackDetailScreen> {
final LocationDatabase _database = LocationDatabase();
TrackDetail? _trackDetail;
bool _isLoading = false;
@override
void initState() {
super.initState();
_loadTrackDetail();
}
Future<void> _loadTrackDetail() async {
setState(() => _isLoading = true);
try {
final trackDetail = await _database.getTrackDetail(widget.trackId);
setState(() {
_trackDetail = trackDetail;
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('加载轨迹详情失败: $e')),
);
} finally {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('轨迹详情'),
backgroundColor: Colors.blue,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _trackDetail == null
? const Center(child: Text('轨迹详情不存在'))
: Column(
children: [
// 轨迹信息
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: Colors.grey[200],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_trackDetail!.track.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text('开始时间: ${_formatDateTime(_trackDetail!.track.startTime)}'),
if (_trackDetail!.track.endTime != null)
Text('结束时间: ${_formatDateTime(_trackDetail!.track.endTime!)}'),
Text('总距离: ${_trackDetail!.track.distance.toStringAsFixed(2)} 米'),
if (_trackDetail!.track.duration != null)
Text('总时长: ${_formatDuration(_trackDetail!.track.duration!)}'),
Text('位置点数: ${_trackDetail!.points.length}'),
],
),
),
// 地图
Expanded(
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: _trackDetail!.points.isNotEmpty
? LatLng(
_trackDetail!.points.first.latitude,
_trackDetail!.points.first.longitude,
)
: const LatLng(39.9042, 116.4074),
zoom: 15.0,
),
polylines: {
Polyline(
polylineId: const PolylineId('track'),
points: _trackDetail!.points.map((point) {
return LatLng(point.latitude, point.longitude);
}).toList(),
color: Colors.blue,
width: 4,
),
},
markers: _trackDetail!.points.isNotEmpty
? {
Marker(
markerId: const MarkerId('start'),
position: LatLng(
_trackDetail!.points.first.latitude,
_trackDetail!.points.first.longitude,
),
infoWindow: const InfoWindow(title: '起点'),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
),
Marker(
markerId: const MarkerId('end'),
position: LatLng(
_trackDetail!.points.last.latitude,
_trackDetail!.points.last.longitude,
),
infoWindow: const InfoWindow(title: '终点'),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
),
}
: {},
),
),
],
),
);
}
String _formatDateTime(DateTime dateTime) {
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}
String _formatDuration(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
if (hours > 0) {
return '${hours}小时${minutes}分钟${seconds}秒';
} else if (minutes > 0) {
return '${minutes}分钟${seconds}秒';
} else {
return '${seconds}秒';
}
}
}第六步:创建主应用界面
dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'widgets/location_tracker_widget.dart';
void main() {
runApp(const GeoTrackerApp());
}
class GeoTrackerApp extends StatelessWidget {
const GeoTrackerApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GeoTracker',
theme: ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.light,
),
home: const MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({Key? key}) : super(key: key);
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
bool _permissionsGranted = false;
bool _isLoading = false;
@override
void initState() {
super.initState();
_checkPermissions();
}
Future<void> _checkPermissions() async {
setState(() => _isLoading = true);
try {
// 检查定位权限
final locationPermission = await Permission.location.status;
final backgroundLocationPermission = await Permission.locationAlways.status;
if (locationPermission.isGranted && backgroundLocationPermission.isGranted) {
setState(() => _permissionsGranted = true);
} else {
setState(() => _permissionsGranted = false);
}
} catch (e) {
setState(() => _permissionsGranted = false);
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _requestPermissions() async {
try {
// 请求定位权限
final locationPermission = await Permission.location.request();
if (locationPermission.isGranted) {
// 请求后台定位权限
final backgroundLocationPermission = await Permission.locationAlways.request();
if (backgroundLocationPermission.isGranted) {
setState(() => _permissionsGranted = true);
} else {
_showPermissionDeniedDialog('后台定位权限被拒绝,应用无法在后台追踪位置');
}
} else {
_showPermissionDeniedDialog('定位权限被拒绝,应用无法获取位置信息');
}
} catch (e) {
_showErrorSnackBar('请求权限失败: $e');
}
}
void _showPermissionDeniedDialog(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('权限被拒绝'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('确定'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
openAppSettings();
},
child: const Text('打开设置'),
),
],
),
);
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (!_permissionsGranted) {
return Scaffold(
appBar: AppBar(
title: const Text('GeoTracker'),
backgroundColor: Colors.blue,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.location_off,
size: 64,
color: Colors.grey,
),
const SizedBox(height: 16),
const Text(
'需要定位权限',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'GeoTracker需要访问您的位置来提供位置追踪服务',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: _requestPermissions,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
child: const Text(
'授予权限',
style: TextStyle(fontSize: 18),
),
),
],
),
),
);
}
return const LocationTrackerWidget();
}
}高级功能实现
1. 位置数据分析
dart
// lib/services/location_analytics.dart
import 'dart:math';
import 'package:geolocator/geolocator.dart';
import 'location_database.dart';
class LocationAnalytics {
final LocationDatabase _database = LocationDatabase();
// 计算轨迹统计信息
Future<TrackStatistics> calculateTrackStatistics(int trackId) async {
final trackDetail = await _database.getTrackDetail(trackId);
if (trackDetail == null || trackDetail.points.isEmpty) {
return TrackStatistics.empty();
}
final points = trackDetail.points;
// 总距离
double totalDistance = 0.0;
for (int i = 1; i < points.length; i++) {
totalDistance += Geolocator.distanceBetween(
points[i - 1].latitude,
points[i - 1].longitude,
points[i].latitude,
points[i].longitude,
);
}
// 总时长
final totalDuration = points.last.timestamp.difference(points.first.timestamp);
// 平均速度
final averageSpeed = totalDuration.inSeconds > 0
? (totalDistance / totalDuration.inSeconds) * 3.6 // km/h
: 0.0;
// 最高速度
double maxSpeed = 0.0;
for (int i = 1; i < points.length; i++) {
final duration = points[i].timestamp.difference(points[i - 1].timestamp).inSeconds;
if (duration > 0) {
final distance = Geolocator.distanceBetween(
points[i - 1].latitude,
points[i - 1].longitude,
points[i].latitude,
points[i].longitude,
);
final speed = (distance / duration) * 3.6; // km/h
maxSpeed = max(maxSpeed, speed);
}
}
// 海拔变化
double totalAscent = 0.0;
double totalDescent = 0.0;
for (int i = 1; i < points.length; i++) {
final altitude1 = points[i - 1].altitude ?? 0;
final altitude2 = points[i].altitude ?? 0;
final elevationChange = altitude2 - altitude1;
if (elevationChange > 0) {
totalAscent += elevationChange;
} else {
totalDescent += elevationChange.abs();
}
}
// 边界框
final minLat = points.map((p) => p.latitude).reduce(min);
final maxLat = points.map((p) => p.latitude).reduce(max);
final minLng = points.map((p) => p.longitude).reduce(min);
final maxLng = points.map((p) => p.longitude).reduce(max);
return TrackStatistics(
totalDistance: totalDistance,
totalDuration: totalDuration,
averageSpeed: averageSpeed,
maxSpeed: maxSpeed,
totalAscent: totalAscent,
totalDescent: totalDescent,
pointCount: points.length,
bounds: LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
),
);
}
// 分析停留点
Future<List<StayPoint>> findStayPoints(int trackId, {int minStayTime = 300}) async {
final trackDetail = await _database.getTrackDetail(trackId);
if (trackDetail == null || trackDetail.points.isEmpty) {
return [];
}
final points = trackDetail.points;
final List<StayPoint> stayPoints = [];
int startIndex = 0;
while (startIndex < points.length - 1) {
final stayPoint = _findStayPoint(points, startIndex, minStayTime);
if (stayPoint != null) {
stayPoints.add(stayPoint);
startIndex = stayPoint.endIndex + 1;
} else {
startIndex++;
}
}
return stayPoints;
}
StayPoint? _findStayPoint(List<LocationRecord> points, int startIndex, int minStayTime) {
if (startIndex >= points.length - 1) return null;
final startLocation = points[startIndex];
int endIndex = startIndex + 1;
// 寻找停留区域的边界
while (endIndex < points.length) {
final distance = Geolocator.distanceBetween(
startLocation.latitude,
startLocation.longitude,
points[endIndex].latitude,
points[endIndex].longitude,
);
if (distance > 50) { // 50米半径
break;
}
endIndex++;
}
// 检查停留时间
if (endIndex > startIndex + 1) {
final duration = points[endIndex - 1].timestamp.difference(startLocation.timestamp);
if (duration.inSeconds >= minStayTime) {
return StayPoint(
startIndex: startIndex,
endIndex: endIndex - 1,
latitude: startLocation.latitude,
longitude: startLocation.longitude,
startTime: startLocation.timestamp,
endTime: points[endIndex - 1].timestamp,
duration: duration,
);
}
}
return null;
}
// 分析速度分布
Future<SpeedDistribution> analyzeSpeedDistribution(int trackId) async {
final trackDetail = await _database.getTrackDetail(trackId);
if (trackDetail == null || trackDetail.points.isEmpty) {
return SpeedDistribution.empty();
}
final points = trackDetail.points;
final List<double> speeds = [];
for (int i = 1; i < points.length; i++) {
final duration = points[i].timestamp.difference(points[i - 1].timestamp).inSeconds;
if (duration > 0) {
final distance = Geolocator.distanceBetween(
points[i - 1].latitude,
points[i - 1].longitude,
points[i].latitude,
points[i].longitude,
);
final speed = (distance / duration) * 3.6; // km/h
speeds.add(speed);
}
}
if (speeds.isEmpty) {
return SpeedDistribution.empty();
}
speeds.sort();
final averageSpeed = speeds.reduce((a, b) => a + b) / speeds.length;
final maxSpeed = speeds.last;
final minSpeed = speeds.first;
// 计算百分位数
final p50 = _percentile(speeds, 0.5);
final p75 = _percentile(speeds, 0.75);
final p90 = _percentile(speeds, 0.9);
final p95 = _percentile(speeds, 0.95);
return SpeedDistribution(
averageSpeed: averageSpeed,
maxSpeed: maxSpeed,
minSpeed: minSpeed,
p50: p50,
p75: p75,
p90: p90,
p95: p95,
sampleCount: speeds.length,
);
}
double _percentile(List<double> sortedList, double percentile) {
if (sortedList.isEmpty) return 0.0;
final index = (sortedList.length - 1) * percentile;
final lower = index.floor();
final upper = index.ceil();
final weight = index % 1;
if (upper >= sortedList.length) {
return sortedList.last;
}
return sortedList[lower] * (1 - weight) + sortedList[upper] * weight;
}
}
// 轨迹统计信息
class TrackStatistics {
final double totalDistance; // 米
final Duration totalDuration;
final double averageSpeed; // km/h
final double maxSpeed; // km/h
final double totalAscent; // 米
final double totalDescent; // 米
final int pointCount;
final LatLngBounds bounds;
TrackStatistics({
required this.totalDistance,
required this.totalDuration,
required this.averageSpeed,
required this.maxSpeed,
required this.totalAscent,
required this.totalDescent,
required this.pointCount,
required this.bounds,
});
factory TrackStatistics.empty() {
return TrackStatistics(
totalDistance: 0.0,
totalDuration: Duration.zero,
averageSpeed: 0.0,
maxSpeed: 0.0,
totalAscent: 0.0,
totalDescent: 0.0,
pointCount: 0,
bounds: LatLngBounds(
southwest: const LatLng(0, 0),
northeast: const LatLng(0, 0),
),
);
}
}
// 停留点
class StayPoint {
final int startIndex;
final int endIndex;
final double latitude;
final double longitude;
final DateTime startTime;
final DateTime endTime;
final Duration duration;
StayPoint({
required this.startIndex,
required this.endIndex,
required this.latitude,
required this.longitude,
required this.startTime,
required this.endTime,
required this.duration,
});
}
// 速度分布
class SpeedDistribution {
final double averageSpeed;
final double maxSpeed;
final double minSpeed;
final double p50; // 中位数
final double p75; // 75百分位
final double p90; // 90百分位
final double p95; // 95百分位
final int sampleCount;
SpeedDistribution({
required this.averageSpeed,
required this.maxSpeed,
required this.minSpeed,
required this.p50,
required this.p75,
required this.p90,
required this.p95,
required this.sampleCount,
});
factory SpeedDistribution.empty() {
return SpeedDistribution(
averageSpeed: 0.0,
maxSpeed: 0.0,
minSpeed: 0.0,
p50: 0.0,
p75: 0.0,
p90: 0.0,
p95: 0.0,
sampleCount: 0,
);
}
}
// 经纬度边界框
class LatLngBounds {
final LatLng southwest;
final LatLng northeast;
LatLngBounds({
required this.southwest,
required this.northeast,
});
}
// 经纬度点
class LatLng {
final double latitude;
final double longitude;
const LatLng(this.latitude, this.longitude);
}2. 位置数据导出
dart
// lib/services/location_exporter.dart
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus';
import 'location_database.dart';
class LocationExporter {
final LocationDatabase _database = LocationDatabase();
// 导出轨迹为GPX格式
Future<File> exportTrackToGPX(int trackId) async {
final trackDetail = await _database.getTrackDetail(trackId);
if (trackDetail == null) {
throw ExportException('轨迹不存在');
}
final gpxContent = _generateGPXContent(trackDetail);
final file = await _saveFile('track_${trackDetail.track.id}.gpx', gpxContent);
return file;
}
// 导出轨迹为KML格式
Future<File> exportTrackToKML(int trackId) async {
final trackDetail = await _database.getTrackDetail(trackId);
if (trackDetail == null) {
throw ExportException('轨迹不存在');
}
final kmlContent = _generateKMLContent(trackDetail);
final file = await _saveFile('track_${trackDetail.track.id}.kml', kmlContent);
return file;
}
// 导出轨迹为JSON格式
Future<File> exportTrackToJSON(int trackId) async {
final trackDetail = await _database.getTrackDetail(trackId);
if (trackDetail == null) {
throw ExportException('轨迹不存在');
}
final jsonContent = _generateJSONContent(trackDetail);
final file = await _saveFile('track_${trackDetail.track.id}.json', jsonContent);
return file;
}
// 导出所有轨迹为CSV格式
Future<File> exportAllTracksToCSV() async {
final tracks = await _database.getTracks();
final csvContent = _generateCSVContent(tracks);
final file = await _saveFile('tracks.csv', csvContent);
return file;
}
// 分享轨迹文件
Future<void> shareTrack(int trackId, String format) async {
File file;
switch (format.toLowerCase()) {
case 'gpx':
file = await exportTrackToGPX(trackId);
break;
case 'kml':
file = await exportTrackToKML(trackId);
break;
case 'json':
file = await exportTrackToJSON(trackId);
break;
default:
throw ExportException('不支持的格式: $format');
}
await Share.shareXFiles([XFile(file.path)], text: '分享轨迹文件');
}
// 生成GPX内容
String _generateGPXContent(TrackDetail trackDetail) {
final buffer = StringBuffer();
buffer.writeln('<?xml version="1.0" encoding="UTF-8"?>');
buffer.writeln('<gpx version="1.1" creator="GeoTracker" xmlns="http://www.topografix.com/GPX/1/1">');
// 轨迹信息
buffer.writeln(' <trk>');
buffer.writeln(' <name>${_escapeXml(trackDetail.track.name)}</name>');
if (trackDetail.track.description != null) {
buffer.writeln(' <desc>${_escapeXml(trackDetail.track.description!)}</desc>');
}
buffer.writeln(' <trkseg>');
// 轨迹点
for (final point in trackDetail.points) {
buffer.writeln(' <trkpt lat="${point.latitude}" lon="${point.longitude}">');
if (point.altitude != null) {
buffer.writeln(' <ele>${point.altitude}</ele>');
}
buffer.writeln(' <time>${_formatGPXTime(point.timestamp)}</time>');
buffer.writeln(' </trkpt>');
}
buffer.writeln(' </trkseg>');
buffer.writeln(' </trk>');
buffer.writeln('</gpx>');
return buffer.toString();
}
// 生成KML内容
String _generateKMLContent(TrackDetail trackDetail) {
final buffer = StringBuffer();
buffer.writeln('<?xml version="1.0" encoding="UTF-8"?>');
buffer.writeln('<kml xmlns="http://www.opengis.net/kml/2.2">');
buffer.writeln(' <Document>');
buffer.writeln(' <name>${_escapeXml(trackDetail.track.name)}</name>');
// 轨迹线
buffer.writeln(' <Placemark>');
buffer.writeln(' <name>${_escapeXml(trackDetail.track.name)}</name>');
buffer.writeln(' <LineString>');
buffer.writeln(' <coordinates>');
for (final point in trackDetail.points) {
buffer.writeln(' ${point.longitude},${point.latitude}${point.altitude != null ? ',${point.altitude}' : ''}');
}
buffer.writeln(' </coordinates>');
buffer.writeln(' </LineString>');
buffer.writeln(' </Placemark>');
// 轨迹点
for (int i = 0; i < trackDetail.points.length; i++) {
final point = trackDetail.points[i];
buffer.writeln(' <Placemark>');
buffer.writeln(' <name>Point ${i + 1}</name>');
buffer.writeln(' <Point>');
buffer.writeln(' <coordinates>${point.longitude},${point.latitude}${point.altitude != null ? ',${point.altitude}' : ''}</coordinates>');
buffer.writeln(' </Point>');
buffer.writeln(' </Placemark>');
}
buffer.writeln(' </Document>');
buffer.writeln('</kml>');
return buffer.toString();
}
// 生成JSON内容
String _generateJSONContent(TrackDetail trackDetail) {
final trackData = {
'track': {
'id': trackDetail.track.id,
'name': trackDetail.track.name,
'description': trackDetail.track.description,
'startTime': trackDetail.track.startTime.toIso8601String(),
'endTime': trackDetail.track.endTime?.toIso8601String(),
'distance': trackDetail.track.distance,
'createdAt': trackDetail.track.createdAt.toIso8601String(),
},
'points': trackDetail.points.map((point) => {
'latitude': point.latitude,
'longitude': point.longitude,
'altitude': point.altitude,
'accuracy': point.accuracy,
'speed': point.speed,
'heading': point.heading,
'timestamp': point.timestamp.toIso8601String(),
'isMocked': point.isMocked,
}).toList(),
};
return const JsonEncoder.withIndent(' ').convert(trackData);
}
// 生成CSV内容
String _generateCSVContent(List<Track> tracks) {
final buffer = StringBuffer();
// CSV头部
buffer.writeln('ID,名称,描述,开始时间,结束时间,距离(米),创建时间');
// 数据行
for (final track in tracks) {
buffer.writeln('${track.id},"${_escapeCsv(track.name)}","${_escapeCsv(track.description ?? '')}","${track.startTime.toIso8601String()}","${track.endTime?.toIso8601String() ?? ''}",${track.distance},"${track.createdAt.toIso8601String()}"');
}
return buffer.toString();
}
// 保存文件
Future<File> _saveFile(String filename, String content) async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/$filename');
return await file.writeAsString(content);
}
// XML转义
String _escapeXml(String text) {
return text
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
// CSV转义
String _escapeCsv(String text) {
if (text.contains(',') || text.contains('"') || text.contains('\n')) {
return '"${text.replaceAll('"', '""')}"';
}
return text;
}
// 格式化GPX时间
String _formatGPXTime(DateTime dateTime) {
final utcTime = dateTime.toUtc();
return '${utcTime.year.toString().padLeft(4, '0')}-${utcTime.month.toString().padLeft(2, '0')}-${utcTime.day.toString().padLeft(2, '0')}T${utcTime.hour.toString().padLeft(2, '0')}:${utcTime.minute.toString().padLeft(2, '0')}:${utcTime.second.toString().padLeft(2, '0')}Z';
}
}
// 导出异常
class ExportException implements Exception {
final String message;
ExportException(this.message);
@override
String toString() => message;
}测试与调试
1. 定位服务测试
dart
// test/location_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:geolocator/geolocator.dart';
import 'package:geo_tracker/services/location_service.dart';
class MockGeolocator extends Mock implements Geolocator {}
void main() {
group('LocationService Tests', () {
late LocationService locationService;
setUp(() {
locationService = LocationService();
});
test('should initialize successfully with granted permissions', () async {
// 模拟定位服务启用和权限授予
when(Geolocator.isLocationServiceEnabled())
.thenAnswer((_) async => true);
when(Geolocator.checkPermission())
.thenAnswer((_) async => LocationPermission.whileInUse);
await expectLater(locationService.initialize(), completes);
});
test('should throw exception when location service is disabled', () async {
// 模拟定位服务未启用
when(Geolocator.isLocationServiceEnabled())
.thenAnswer((_) async => false);
await expectLater(
locationService.initialize(),
throwsA(isA<LocationServiceException>()),
);
});
test('should start tracking successfully', () async {
// 模拟权限和定位服务正常
when(Geolocator.isLocationServiceEnabled())
.thenAnswer((_) async => true);
when(Geolocator.checkPermission())
.thenAnswer((_) async => LocationPermission.whileInUse);
when(Geolocator.getCurrentPosition())
.thenAnswer((_) async => Position(
latitude: 39.9042,
longitude: 116.4074,
timestamp: DateTime.now(),
accuracy: 10,
altitude: 0,
heading: 0,
speed: 0,
isMocked: false,
));
await locationService.initialize();
await expectLater(locationService.startTracking(), completes);
});
});
}2. 地理围栏测试
dart
// test/geofence_manager_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:geo_tracker/services/geofence_manager.dart';
void main() {
group('GeofenceManager Tests', () {
late GeofenceManager geofenceManager;
setUp(() {
geofenceManager = GeofenceManager();
});
test('should add geofence successfully', () async {
final geofence = GeofenceRegion(
id: 'test1',
name: 'Test Geofence',
latitude: 39.9042,
longitude: 116.4074,
radius: 100,
);
await expectLater(geofenceManager.addGeofence(geofence), completes);
expect(geofenceManager.geofences.length, equals(1));
expect(geofenceManager.geofences.first.id, equals('test1'));
});
test('should throw exception when adding duplicate geofence', () async {
final geofence1 = GeofenceRegion(
id: 'test1',
name: 'Test Geofence 1',
latitude: 39.9042,
longitude: 116.4074,
radius: 100,
);
final geofence2 = GeofenceRegion(
id: 'test1',
name: 'Test Geofence 2',
latitude: 39.9042,
longitude: 116.4074,
radius: 200,
);
await geofenceManager.addGeofence(geofence1);
await expectLater(
geofenceManager.addGeofence(geofence2),
throwsA(isA<GeofenceException>()),
);
});
test('should remove geofence successfully', () async {
final geofence = GeofenceRegion(
id: 'test1',
name: 'Test Geofence',
latitude: 39.9042,
longitude: 116.4074,
radius: 100,
);
await geofenceManager.addGeofence(geofence);
await geofenceManager.removeGeofence('test1');
expect(geofenceManager.geofences.length, equals(0));
});
});
}最佳实践与注意事项
1. 权限管理
- 渐进式权限请求:先请求基本定位权限,再请求后台定位权限
- 权限说明:清晰地向用户解释为什么需要定位权限
- 优雅降级:在权限被拒绝时提供替代功能
2. 电池优化
- 合理设置更新频率:根据应用需求调整位置更新间隔
- 使用后台服务:在 Android 上使用前台服务确保后台定位
- 智能暂停:在用户不活动时暂停位置更新
3. 数据管理
- 本地缓存:使用 SQLite 数据库存储位置数据
- 数据清理:定期清理过期的位置数据
- 数据压缩:对轨迹数据进行压缩以减少存储空间
4. 用户体验
- 可视化反馈:在地图上清晰显示当前位置和轨迹
- 状态指示:明确显示定位服务的状态
- 离线支持:支持离线地图和轨迹查看
5. 隐私保护
- 数据加密:对敏感位置数据进行加密存储
- 用户控制:允许用户控制位置数据的收集和使用
- 数据最小化:只收集必要的位置信息
总结
通过本文的详细介绍,我们成功实现了一个功能完整的位置追踪应用 GeoTracker。这个项目涵盖了:
- 定位服务基础架构:设计了完整的定位服务管理架构
- 地理围栏功能:实现了地理围栏的创建、监控和事件处理
- 数据存储与分析:提供了位置数据的存储、分析和导出功能
- 用户界面设计:创建了直观的位置追踪和轨迹查看界面
- 高级功能:实现了位置数据分析、停留点检测和数据导出
- 测试与调试:提供了完整的测试方案
定位服务是移动应用开发中的重要功能,通过 Flutter 的桥接能力,我们可以轻松实现跨平台的定位功能。在实际项目中,还可以根据具体需求进一步扩展功能,比如:
- 集成更多地图服务(如高德地图、百度地图)
- 添加实时位置共享功能
- 实现轨迹回放和动画效果
- 集成天气信息和 POI 搜索
- 添加运动类型识别和卡路里计算
希望本文能够帮助开发者更好地理解和实现 Flutter 中的定位服务功能。

