[译]Flutter地理位置插件geolocator

4,636 阅读11分钟

「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战」。

今天介绍一个 Flutter 的地理位置插件 geolocator。

下面内容肉翻自 geolocator 的 pub,水平有限,不吝赐教。

geolocator

geolocator 简介

geolocator是一个 Flutter 的地理位置插件,提供了对各平台特定的地理位置服务的简易访问途径。

特性

  • 获取上一次的已知位置
  • 获取设备的当前位置
  • 获取持续的位置更新
  • 检查设备的位置服务是否可用
  • 计算两个地理坐标之间的距离(单位:米)
  • 计算两个地理坐标之间的方位

重要: geolocator插件的7.0.0版本包含了一些不兼容的改动,完整的概览可以看一下 Breaking changes in 7.0.0 的 wiki。
从6.0.0版本开始的 geocoding 特性(placemarkFromAddress 和 placemarkFromCoordinates)不再是 geolocator 的一部分。这些特性已经转移到单独的 geocoding 插件中。这个新插件是旧方法的升级版。

使用

参考 安装说明 把 geolocator 插件添加到 Flutter 应用中。 为了使 geolocator 插件在各种设备上能正常工作,需要做如下设置:

Android

升级1.12之前的 Android 工程

从5.0.0版本开始,插件的实现使用了 Flutter 1.12 的 Android 插件的 API。 不幸的是,这意味着 APP 开发者也需要迁移他们的 APP 以支持新的 Android 架构。可以按照 Upgrading pre 1.12 Android projects 的迁移向导来迁移。迁移失败的话,使用新的插件有可能导致无法预期的行为。

AndroidX

geolocator 插件需要 Android Support Libraries 的 AndroidX 版本。 这意味着需要确认你的 Android 工程支持 AndroidX。 详细说明可以参考这里

内容摘要:

  1. gradle.properties 文件里添加下面的内容。
android.useAndroidX=true
android.enableJetifier=true
  1. 确保 android/app/build.gradle 中的 compileSdkVersion 设定为 31。
android {
  compileSdkVersion 31

  ...
}
  1. 确保所有 android. 的依赖已替换为相应的 AndroidX 版本。
    完整的列表在此:Migrating to AndroidX

权限

在 Android 中需要添加 ACCESS_COARSE_LOCATION 或者 ACCESS_FINE_LOCATION 权限到 Android Manifest 里。 打开 AndroidManifest.xml 文件(位置:android/app/src/main),添加下面两行中的一行作为 <manifest> 标签的直接子项。 如果两个权限都被配置的话,geolocator 插件会使用 ACCESS_FINE_LOCATION 权限。

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

从 Android10 开始,如果 APP 在后台运行时想要持续获取(地理位置信息的)更新(注意 geolocator 插件在后台运行时不支持接收一个处理中的位置更新),需要添加 ACCESS_BACKGROUND_LOCATION 权限( 在 ACCESS_COARSE_LOCATIONACCESS_FINE_LOCATION 权限下面)。

<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

注: 指定 ACCESS_COARSE_LOCATION 权限会得到和一个城市街区大致相等精度的位置更新。 在获取第一次的位置修正之前可能会花很长时间(几分钟), 因为 ACCESS_COARSE_LOCATION 只使用网络服务来计算设备的位置。更多信息参考这里

iOS

在 iOS 中需要在 Info.plist 文件(位置:ios/Runner)中添加以下内容以访问设备的位置。
简单来说,就是打开 Info.plist 文件,添加下面的内容 (确保已更新描述内容使其符合 APP 的语境):

<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to location when open.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to location when in the background.</string>

如果 APP 在后台运行时想要持续获取(地理位置的)更新,需要向 XCode 工程中添加 Backgroud Modes capability(Project > Signing and Capabilities > "+ Capability" 按钮)和选中位置更新。 如此设置需要当心的是,当提交 App 到 AppStore 时,需要向 Apple 解释为什么你的 APP 需要如此设置的细节。如果 Apple 不满意你的解释,你的 APP 会被拒。

使用 requestTemporaryFullAccuracy({purposeKey: "YourPurposeKey"}) 方法时,需要向 Info.plist 文件中添加下面的 dictionary。

<key>NSLocationTemporaryUsageDescriptionDictionary</key>
<dict>
  <key>YourPurposeKey</key>
  <string>The example App requires temporary access to the device&apos;s precise location.</string>
</dict>

第二个 Key (此例中是 YourPurposeKey )需要和传递给 requestTemporaryFullAccuracy() 方法的参数的 purposeKey 匹配。 也可以定义针对 APP 的不同特性定义多个 Key 。 更多信息可在这里找到。

注: 第一次临时请求完整精度访问会花很长时间来显示确认弹框。这是因为 iOS 确认精确的用户位置需要花很长时间。不幸的是,这个是插件无法控制的。

macOS

在 macOS 中需要在 Info.plist 文件(位置:macOS/Runner)中添加以下内容以访问设备的位置。
简单来说,就是打开 Info.plist 文件,添加下面的内容 (确保已更新描述内容使其符合 APP 的语境):

<key>NSLocationUsageDescription</key>
<string>This app needs access to location.</string>

同时需要在 DebugProfile.entitlements 和 Release.entitlements 中添加以下内容。 这用来声明 APP 想要使用设备的位置服务。 APP 会被添加到 系统偏好设置 -> 安全性与隐私 -> 隐私 设定的列表中。

<key>com.apple.security.personal-information.location</key>
<true/>

使用 requestTemporaryFullAccuracy({purposeKey: "YourPurposeKey"}) 方法时,需要向 Info.plist 文件中添加下面的 dictionary。

<key>NSLocationTemporaryUsageDescriptionDictionary</key>
<dict>
  <key>YourPurposeKey</key>
  <string>The example App requires temporary access to the device&apos;s precise location.</string>
</dict>

第二个 Key (此例中是 YourPurposeKey )需要和传递给 requestTemporaryFullAccuracy() 方法的参数的 purposeKey 匹配。 也可以定义针对 APP 的不同特性定义多个 Key 。 更多信息可在这里找到。

注: 第一次临时请求完整精度访问会花很长时间来显示确认弹框。这是因为 macOS 确认精确的用户位置需要花很长时间。不幸的是,这个是插件无法控制的。

Web

要在 Web 中使用 geolocator 插件,需要 Flutter 1.20或更高版本。 当向 pubspec.yaml 里添加geolocator: ^6.2.0 的依赖时, Flutter 会自动添加 geolocator_web 包到应用中。

以下的 geolocator API 的方法没有在 Web 中支持。 如果使用的话,会引发带着 UNSUPPORTED_OPERATION 错误码的 PlatformException 异常。

  • getLastKnownPosition({ bool forceAndroidLocationManager = true })

  • openAppSettings()

  • openLocationSettings()

    注: 由于 dart:html 库里的一个 bug ,Web 版的 geolocator 插件在空安全模式下,无法正常使用,也无法在 release 模式下编译。 在 release 模式下运行 APP 会引发 Uncaught TypeError (issue #693)。当前的变通方法是在 release 模式下禁用空安全来编译。

      flutter build web --no-sound-null-safety --release
    

    Dart 团队已经进行了修正(当前在 Dart 2.15.0-63.0.dev 版本中已可用),当然也已经发布并集成到了 Flutter 中。

示例

以下代码展示了如何请求设备当前位置,包括检查位置服务是否可用 和请求访问设备的位置权限。

import 'package:geolocator/geolocator.dart';

/// Determine the current position of the device.
///
/// When the location services are not enabled or permissions
/// are denied the `Future` will return an error.
/// 
/// 确定设备的当前位置
/// 当位置服务不可用或权限被拒绝时,会返回 error。
Future<Position> _determinePosition() async {
  bool serviceEnabled;
  LocationPermission permission;

  // Test if location services are enabled.
  // 检查位置服务是否可用。
  serviceEnabled = await Geolocator.isLocationServiceEnabled();
  if (!serviceEnabled) {
    // Location services are not enabled don't continue
    // accessing the position and request users of the 
    // App to enable the location services.
    // 位置服务不可用,不再继续访问位置,也不会向 APP 的用户请求允许使用位置服务。
    return Future.error('Location services are disabled.');
  }

  permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
    if (permission == LocationPermission.denied) {
      // Permissions are denied, next time you could try
      // requesting permissions again (this is also where
      // Android's shouldShowRequestPermissionRationale 
      // returned true. According to Android guidelines
      // your App should show an explanatory UI now.
      // 权限被拒绝,下次会继续尝试请求权限(安卓的shouldShowRequestPermissionRationale也会返回 true 。
      // 按照安卓的指南,这时APP 会显示一个有说明信息的界面。)
      return Future.error('Location permissions are denied');
    }
  }
  
  if (permission == LocationPermission.deniedForever) {
    // Permissions are denied forever, handle appropriately. 
    // 权限被永久拒绝,进行适当处理。
    return Future.error(
      'Location permissions are permanently denied, we cannot request permissions.');
  } 

  // When we reach here, permissions are granted and we can
  // continue accessing the position of the device.
  // 至此说明已被授权,可以继续访问设备的位置了。
  return await Geolocator.getCurrentPosition();
}

API

地理位置

查询设备的当前位置,只需调用 getCurrentPosition 方法。 可通过以下参数来调整结果:

  • desiredAccuracy: APP 想要接收到的位置数据的精度。
  • timeLimit: 请求当前位置允许的最大时间。超过这个时间时,会抛出 TimeOutException 异常,调用会被取消。默认无超时时间。
import 'package:geolocator/geolocator.dart';

Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);

查询在设备中存储的上一次已知的位置,可以使用 getLastKnownPosition 方法(如果没有位置详细可用,该方法会返回 null ):

import 'package:geolocator/geolocator.dart';

Position position = await Geolocator.getLastKnownPosition();

监听位置变化,可以调用 getPositionStream 来接收用来监听和接收位置更新的流。可通过以下参数来调整结果:

  • desiredAccuracy: APP 想要接收到的位置数据的精度。
  • distanceFilter: 触发位置更新事件所需的设备水平移动的最小距离(单位:米)
  • timeInterval: (仅 Android ),下次触发位置更新事件所需的最小时间间隔。
  • timeLimit: 两次位置更新之间允许的最大时间。超过这个时间时,会抛出TimeOutException异常,流会被取消。默认无超时时间。
import 'package:geolocator/geolocator.dart';

StreamSubscription<Position> positionStream = Geolocator.getPositionStream(locationOptions).listen(
    (Position position) {
        print(position == null ? 'Unknown' : position.latitude.toString() + ', ' + position.longitude.toString());
    });

监听服务状态,可以调用 getServiceStatusStream 方法。这个方法会返回一个 Stream<ServiceStatus> ,可被监听,然后接收位置服务状态的更新。

import 'package:geolocator/geolocator.dart';

StreamSubscription<ServiceStatus> serviceStatusStream = Geolocator.getServiceStatusStream().listen(
    (ServiceStatus status) {
        print(status);
    });

获取位置精度 (仅 Android 和 iOS 14+)

如果想要知道用户能获取大体的位置还是能获取精确的位置,可以调用 Geolocator().getLocationAccuracy() 方法。这个方法会返回 Future<LocationAccuracyStatus> 。当用户能获取大体的位置时,返回值包含 LocationAccuracyStatus.reduced ;当用户能获取精确的位置时,返回值包含 LocationAccuracyStatus.precise 。如果在赋予用户权限之前调用 getLocationAccuracy ,会默认返回 LocationAccuracyStatus.reduced 。 iOS 13 或更低版本, getLocationAccuracy 总是会返回 LocationAccuracyStatus.precise ,因为这是 iOS 13 或更低版本的默认值。

import 'package:geolocator/geolocator.dart';

var accuracy = await Geolocator.getLocationAccuracy();

检查服务位置是否可用,可以调用 isLocationServiceEnabled 方法:

import 'package:geolocator/geolocator.dart';

bool isLocationServiceEnabled  = await Geolocator.isLocationServiceEnabled();

权限

在你尝试通过 getCurrentPosition 或者 getPositionStream 方法获得位置时,geolocator 会自动尝试请求权限。 即使这样,我们也提供了允许手动请求权限的方法。

如果想要检查用户是否已被授权获得位置,可以调用 checkPermission 方法。

import 'package:geolocator/geolocator.dart';

LocationPermission permission = await Geolocator.checkPermission();

如果想要请求访问设备位置的权限,可以调用 requestPermission 方法。

import 'package:geolocator/geolocator.dart';

LocationPermission permission = await Geolocator.requestPermission();

checkPermissionrequestPermission 方法可能的结果如下:

权限说明
denied访问设备位置的权限已被用户拒绝。可以再次请求权限(这也是初始的权限状态)
deniedForever访问设备位置的权限已被永久拒绝。直到用户在 APP 设定里更新权限之前,请求权限的许可对话框不会再表示。
whileInUse仅在使用 APP 时允许访问设备位置。
always即使 APP 在后台运行也允许访问设备位置。

注:在 Android 上检查权限时只会返回 whileInUseralways 或者 denied 。这是由于受限于在 Android 操作系统上,检查权限时,无法确认权限是否是被永久拒绝。可用的变通方法只能是让 requestPermission 方法返回这样的结果。 更多信息可以在我们的 wiki 上找到。

设定

在一些情况下,有必要向用户请求权限和更新设备的设定。例如用户一开始永久拒绝了访问设备位置的权限,或者位置服务不可用(还有,在 Android 上,自动解析不工作)。这些情况下,你可以使用 openAppSettingsopenLocationSettings 方法来让用户立即跳转到设备的设定界面。

在 Android 上,openAppSettings 方法会跳转到 APP 专有的设定,在这里用户可以更新必要的权限。openLocationSettings 方法会跳转到位置设定,在这里用户可以允许使用/禁用位置服务。

在 iOS 上,不允许打开特定的设定界面,所以两个方法都会跳转到设定APP( iOS系统的设定),在这里,用户可以浏览和修正设定种类来更新权限或者允许使用/禁用位置服务。

import 'package:geolocator/geolocator.dart';

await Geolocator.openAppSettings();
await Geolocator.openLocationSettings();

实用方法

要计算两个地理坐标之间的距离(单位:米),可以使用 distanceBetween 方法。该方法有四个参数:

参数类型说明
startLatitudedouble开始位置的纬度
startLongitudedouble开始位置的经度
endLatitudedouble开始位置的纬度
endLongitudedouble开始位置的经度
import 'package:geolocator/geolocator.dart';

double distanceInMeters = Geolocator.distanceBetween(52.2165157, 6.9437819, 52.3546274, 4.8285838);

要计算两个地理坐标之间的方位,可以使用 bearingBetween方法。该方法也有四个参数:

参数类型说明
startLatitudedouble开始位置的纬度
startLongitudedouble开始位置的经度
endLatitudedouble开始位置的纬度
endLongitudedouble开始位置的经度
import 'package:geolocator/geolocator.dart';

double bearing = Geolocator.bearingBetween(52.2165157, 6.9437819, 52.3546274, 4.8285838);

位置精度

下表列出了各平台的精度选项

AndroidiOS
lowest500m3000m
low500m1000m
medium100m - 500m100m
high0 - 100m10m
best0 - 100m~0m
bestForNavigation0 - 100mOptimized for navigation

问题联系

有任何要提交的问题、bug 或者特性,可以在我们的 GitHub 页面上提交 issue 。需要商业支持,可以邮件联系我们 hello@baseflow.com

有贡献意向

如果想对该插件有所贡献(例如:改进文档、解决 bug 、或者添加酷的新特性),请仔细检阅我们的 贡献指南,然后发送你的 pull request 给我们。

作者

适用于 Flutter 的 geolocator 插件是由 Baseflow 开发 。