Flutter でバックグラウンドでの位置情報の取得の許可を取得する

Flutter では geolocator を使ってバックグラウンドでの位置情報を取得することができます。そのためには権限を取得する必要がありますが、Android と iOS では単純にその権限を取得することができません。この記事ではその仕様のややこしい点について説明します。

「常に許可」の権限の取得について

バックグラウンドで位置情報を取得するためには、通常の権限である iOS の アプリの使用中は許可 (Android の アプリの使用時のみ) に加え、常に許可 の権限を取得する必要があります。

そのため、まず通常の位置情報の権限を取得した後に、常に許可の権限を取得するというステップを踏むことになります。

サンプルコード

Flutter で権限を取得する処理を簡単に書くことができるパッケージ permission_handler を使ったコードを紹介します。Android ではバックグラウンドで処理を継続する際に通知を表示するために、通知の権限も取得します。

Future<bool> requestPermissions() async {
  // Android の場合は位置情報の取得権限と通知の許可が必要
  if (Platform.isAndroid) {
    if (await Permission.notification.request() != PermissionStatus.granted) {
      return false;
    }
  }
  // locationAlways の権限を取得するために、先にアプリ使用中の位置情報取得権限を取得する必要がある
  if (await Permission.location.request() != PermissionStatus.granted) {
    return false;
  }
  // アプリ使用中の位置情報取得権限が取得できたら、バックグラウンドでの位置情報取得権限を取得する
  // Android では設定画面が開かれる
  // iOS ではダイアログが表示されるが、await してるがすぐに denied が返ってくる
  // https://github.com/Baseflow/flutter-permission-handler/issues/1152
  if (await Permission.locationAlways.request() != PermissionStatus.granted) {
    return false;
  }
  return true;
}

locationAlways の request 時に、Android ではアプリ内で OS のアプリ設定画面に遷移します。戻るボタンを押すことでアプリの画面に戻ることができます。iOS では許可のポップアップが表示されます。

locationAlways.request() を await で待てない問題

ところで、iOS では permission_handler の問題で、ポップアップのボタンをユーザーがタップする前に locationAlways.request() が即座に終了してしまいます。

github.com

ワークアラウンドとして、didChangeAppLifecycleState を使ってアプリがアクティブになった際に権限をチェックする方法があります。

バックグラウンド動作の設定

iOS

Info.plist に下記を追記します

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Accessing location information to measure driving spatial cognition.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Accessing location information to measure driving spatial cognition.</string>
<key>UIBackgroundModes</key>
<array>
  <string>location</string>
</array>

Android

AndroidManifest.xml に下記を追記します

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

位置情報の取得

geolocator パッケージを使ってバックグラウンドで位置情報を取得するコードです。LocationSettings で Android, iOS ごとに設定を行う必要があります。

Stream<Position> getPositionStream() {
    LocationSettings locationSettings;
    if (Platform.isAndroid) {
      locationSettings = AndroidSettings(
        foregroundNotificationConfig: const ForegroundNotificationConfig(
          notificationTitle: "通知のタイトル",
          notificationText: '通知の本文',
        ),
      );
    } else if (Platform.isIOS) {
      locationSettings = AppleSettings(
        showBackgroundLocationIndicator: true,
        allowBackgroundLocationUpdates: true,
      );
    } else {
      throw UnsupportedError('Unsupported platform');
    }

    return Geolocator.getPositionStream(locationSettings: locationSettings);
}