当社で「チーム・ウォーク」という歩数共有アプリ(iOS/Android対応)をFlutterベースで開発して今も公開中です。
このプロダクトではスマホ端末内で歩数の情報を取得する必要がありましたので、Flutterでヘルスケア情報が取得可能なパッケージ"health"を利用しています。
このパッケージ、取得されるデータ構造や取得方法に要注意ポイントが多数あると感じますので、本記事で共有したいと思います。
◇目次
これを知らないとハマる可能性大:healthパッケージの歩数データへのアクセス許諾機構
healthパッケージのデータ取得許諾プロセスは、iOSの時とAndroidの場合で違っています。(尚、前提として歩数情報はプライバシーデータに該当するため、いずれのプラットフォームも一定の慎重さで取り扱っています。)
iOSの場合は端末内でユーザーに許諾を取りヘルスケアアプリからデータ取得するので開発者にとっては比較的シンプルですが、Androidの場合はGoogleアカウントに紐づけて許諾を行うため、サーバーへのアクセスが必須です。
Android端末でもGoogle Fitという、iOSでいうヘルスケアアプリに近しいものが存在しているので、何となく同じ感じで端末内にデータがあるのだろうと思ってかかると、理解しがたい事象にぶちあたると思います。
AppleとGoogleで、センシティブデータアクセスの厳正さのアプローチが異なる
歩数情報を含むアクティビティは気密性の高い個人情報にあたるので厳正に取り扱うべき、という基本的な考え方はいずれのプラットフォームも共通しています。ただその厳正さのアプローチが異なっています。
iOS(Apple)側はなぜアクセス権が必要なのかの説明もAppレビューに含まれている一方、Android(Google)は別途審査を行う形式としています。
また、Googleはユーザーへの説明に動画を用いるなど、その手厚さではAppleよりGoogleの方が厳しい度合いが高い(=デベロッパの手間がかかる)と言えます。
AndroidはOAuth同意画面の審査に、アプリ審査とは別途4~5週間は見込む必要
Androidの場合、上述の通りアプリとは別途、GoogleによるGoogle Fit APIのOAuth同意画面審査が必要です。
Youtubeでのデータ使途の説明動画等が必要の上、細かい指摘でリジェクトが返ってくるので、感触的にはアプリ公開審査より数段高い難易度です。
この審査作業についてGoogle側も「4〜5週間はかかる」と認める通り、とにかく時間がかかります。このプロセスのリードタイムは、アプリ開発において必ず見込むようにしましょう。
この審査に関しては、以下に詳細記事を作成しています。実録も含めて公開していますので、これから審査に挑む方は合わせてご参考ください。
また、2023年現在、Googleによる審査を突破後に更にCASA Tier2スキャンを受けて認証される必要があります。このプロセスについても別途記事を作成しています。
※所用期間・工数等は目安であり、実際は前後する可能性があります
※iOSでのヘルスケアアプリデータへのアクセスは別段の審査は不要です。
(通常のアプリを同じようにGoogle Play Consoleへの公開を申請)
(GCPでプロジェクトを作成して申請。動画やUI修正など細かい指示が入るため、時間・工数共に重め。)
(特に大きな工数が生じるわけではないが、時間はかかる。2022年下旬に追加されたプロセス。)
プラットフォーム別公開フローチャート
iOSとAndroidのプラットフォーム別に公開に向けた手順を表にしています。見ての通りOAuth同意画面の作業がある分、Androidの方が一手多いというのが全体感になります。
iOS | Android |
---|---|
info.plistに権限記述 | AndroidManifest.xmlに権限記述 |
DartでHealthからのデータ取得実装 | DartでHealthからのデータ取得実装 |
アプリ公開審査の通過・公開 | アプリ公開審査の通過・公開 |
サービスデプロイ | OAuth同意画面の作成・申請 |
- | CASA2 Tier2スキャン |
- | サービスデプロイ |
healthパッケージでデータを取得する準備
このセクションの内容については、healthのpub.devに詳しく記述されています
上述のhealthパッケージのデータソースの話が分かっていれば、何が書いてあるのかわかると思います。
iOSのセッティング
info.plistに
- NSHealthShareUsageDescription
- NSHealthUpdateUsageDescription
の2つを追加。Updateしない場合でも、両方加えなければクラッシュします。
Androidのセッティング
AndroidManifest.xmlの権限記述
AndroidManifest.xmlファイルに以下の権限を記述します。
AndroidManifest.xml
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>
<!--ヘルスコネクトに接続する場合は、次の通りandroid.permission.health系も必要。-->
<uses-permission android:name="android.permission.health.READ_STEPS"/>
ファイル冒頭からの記述は次のようになります。
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="XXXXXXXXXXXX">
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<!--ヘルスコネクトに接続する場合は、次の通りandroid.permission.health系も必要。-->
<uses-permission android:name="android.permission.health.READ_STEPS"/>
(以下略)
尚、healthパッケージの中で位置情報等を使用するパラメータを使用する場合は、追加の権限記述が必要になります。詳しくはhealthのpub.devページをご覧ください。
また、ヘルスコネクト用のandroid.permission.health系統については、データ型のリスト | Android デベロッパーに一覧が掲載されています。(ヘルスコネクトについては後述します。)
OAuth同意画面の作成
GCPコンソールでのAPI認証情報とOAath同意画面を作成。GCPコンソールの左側のメニューの「APIとサービス」の中にあります。Android側については再掲の詳細記事に設定方法を詳しく述べていますのでご参考ください。
データ取得のFlutter実装コード
ようやくデータを取得する準備ができたので、コードを書いていきます。このセクションでは、本日一杯の歩数データを取得しています。
1. パッケージインスタンスの初期化(追記)
healthパッケージはシングルトン形式で動作するため、インスタンスを事前に初期化する必要があります。
尚、パッケージのバージョン10.0.0(リリース)にて、healthパッケージがHealthFactoryからシングルトン形式に移行したとの破壊的変更がアナウンスされています。これにより、従前のHealthFactoryでインスタンスを作るのではなく単一インスタンスで動作(シングルトン)する形となります。以前のバージョンで動作していたアプリはコードの修正が必要です。
BREAKING The plugin now works as a singleton using Health() to access it (instead of creating an instance of HealthFactory).
This entails that the plugin now need to be configured using the configure() method before use.
(簡易訳:このプラグインは、HealthFactory のインスタンスを作成する代わりにHealth() を使用してアクセスするシングルトンとして機能するようになりました。これにより、プラグインを使用する前に、configure() メソッドを使用して設定する必要があります。
引用:health Changelog 10.0.0 (pub.dev)
pub.devの例に従い、まずはインスタンスを初期化します。healthパッケージを使用する前のどこか(例ではinitState)でコールしておきましょう。尚、useHealthConnectIfAvailable引数もこの段階で指定します。useHealthConnectIfAvailableの設定については後述します。
以下の様に、インスタンスを初期化します。以降、初期化済みインスタンスをHealth()で取得してパッケージの機能を使用します。
Dart
void initState() {
// configure the health plugin before use.
Health().configure(useHealthConnectIfAvailable: true); //これ。useHealthConnectIfAvailable引数はアプリの方針でオンオフを指定してください。
super.initState();
}
2. 権限リクエスト
iOSでは以前より、またAndroidでは12以降、アクティビティデータへのアクセス権限をユーザーに求める形式となっています。パッケージ公式でも推奨されている通りpermission_handlerパッケージを利用します。
Dart
import 'package:permission_handler/permission_handler.dart';
import 'package:health/health.dart';
//データ種類を指定
final types = [HealthDataType.STEPS];
//権限を確認
final permissionResult = await Health().hasPermissions(types);
//権限がなければユーザーに権限リクエスト(permission_handlerパッケージを使用)
if (permissionResult == false) {
final permissionStatus = await Permission.activityRecognition.request();
}
尚、上述の通りHealth()はシングルトンのインスタンスを取得します。バージョン9.0.0では、以下のコードで生成したインスタンスをHealth()に置き換えてください。
Dart
//healthパッケージバージョン9.0.0以前の場合(旧バージョン)
HealthFactory health = HealthFactory();
3. データ取得プロセス
期間と種類を指定して、HealthFactoryクラスのHealth().getHealthDataFromTypesをコールします。
Dart
import 'package:health/health.dart';
final now = DateTime.now();
final List<HealthDataPoint> healthData = await Health().getHealthDataFromTypes(
DateTime(now.year, now.month, now.day, 0, 0, 0),
DateTime(now.year, now.month, now.day, 23, 59, 59),
[HealthDataType.STEPS]
);
尚、healthパッケージバージョン9.0.0以前ではHealthFactoryインスタンスを生成して使用します。
Dart
//healthパッケージバージョン9.0.0以前の場合(旧バージョン)
import 'package:health/health.dart';
HealthFactory health = HealthFactory();
final now = DateTime.now();
final List<HealthDataPoint> healthData = await health.getHealthDataFromTypes(
DateTime(now.year, now.month, now.day, 0, 0, 0),
DateTime(now.year, now.month, now.day, 23, 59, 59),
[HealthDataType.STEPS]
);
初回アクセス時に、アクセス許可のユーザー認証画面が起動します。尚、ユーザーが拒否した場合、空の配列が返ってきます。忘れずに拾って処理しましょう。
データが取得できない場合
データがうまく取得できないようであれば、ほとんどの場合は権限設定のミスと思われます。ここまでで解説した権限設定を見直しましょう。
あるいはAndroidの場は、上で示した図の通り権限取得の際にAppバンドルの署名情報を検証しています。よってAndroidのデバッグビルドなどで署名情報を含んでいない場合はデータの取得ができないので注意が必要です。
データ取得実装時における要注意ポイント
データ構造をよく見ていれば分かるんですが、デバッグでは気づきにくい点を紹介します。
きちんと意識して実装しないと、正確でない歩数を抽出してしまう可能性があります。
下記の画像内にあるデータ構造からわかる通り、基本的にはsourceIdプロパティで判断すればOKです。
複数の端末からデータが取得される
getHealthDataFromTypesメソッド(上記コード)のデータ取得ですが、iOSでは同一AppleIDで複数端末を持っている人がいると、それぞれの端末が混ざった配列が返ってきます。
下の図でsourceNameを見ればわかる通り、2種類の端末のデータを拾っています。何も考えずに全部足すと約2倍になってしまいます。
その他のフィットネス端末からもデータが取得される
ヘルスケア・Google Fitアプリに連携しているフィットネス端末があると、そちらからもデータが取得されデータに入ってきます。下図の通りsourceIdで判断可能です。目的により集計処理を実装しましょう。
尚、ヘルスケア・Google Fitアプリでユーザーに表示される歩数は、他のデバイスも含んだ集計結果が表示される場合があります。したがって、iOSの場合はcom.apple.healthのみを集計すると、集計結果とヘルスケアアプリの表示内容に差異が生じます。
また、iOSでApple Watchを併用していると、厄介なことにApple Watch分のデータもsourceIdがcom.apple.health.~で取得されます。
こうなるとsourceNameでしか判定不能ですが、このカラムはユーザー側で変更可能なので実装上の判定には使用しにくいのが難点です。
幸いにしてdateToが同一で入ってくる性質があるように見えますので、同一のdateToは弾く等で重複カウントを避けられる可能性があります。(鋭意調査中です)
どうも、上記のように考えていましたがdateToが数秒ずれることも多いようです。なので妥協案としては、FromとToの期間重複を避ける形で積算して集計するのが現実的な手法だと考えられます。(引き続き鋭意調査中です)
ヘルスケア・Google Fitにはユーザーが歩数を直接入力でき、Healthライブラリはそのデータも抽出する
ヘルスケアアプリには、歩数を手動で入力して追加する機能があります。
現行バージョンではsourceIdがcom.app.Health(Healthの先頭が大文字)、sourceNameが「ヘルスケア」(言語設定により変わります。ちなみに英語の場合は「Health」)となりますので注意しましょう。
歩数の重複除けに役立つgetTotalStepsInInterval関数の活用に関する考察
Dart
final healthData = await Health().getTotalStepsInInterval(from, to);
尚、healthパッケージバージョン9.0.0以前(旧バージョン)ではHealthFactoryインスタンスを生成して使用します。
Dart
//healthパッケージバージョン9.0.0(旧バージョン)
final HealthFactory health = HealthFactory();
final healthData = await health.getTotalStepsInInterval(from, to);
HealthFactory.getTotalStepsInInterval関数は、上述の歩数重複の問題をクリアーし、ヘルスケア・Google Fitと同じ値を取得することができます。ウェアラブルデバイスを含んだ歩数データを取得するには一番良い手段といえます。
他方で、この関数では上述の手入力データも含んで取得されてしまいます。手入力データが含まれることで問題がある場合は、別の手段を考える必要があるでしょう。
Androidのヘルスコネクトへの移行対応(2023年7月)
2023年7月現在、GoogleよりFit Android APIの非推奨化とヘルスコネクトへの移行が勧告されています。(参考:移行ガイド | Androidデベロッパー)
ヘルスコネクトはGoogleによるプロダクトでありGoogle Play Storeからアプリのようにダウンロードできますが、動作としては様々なアプリからのヘルスケアデーアを集約してAPIを提供するプラグインのような働きをします。Google Fitからの歩数等連携はもちろん、FitbitやS Healthといったプロダクトのデータも連携可能となります。
healthパッケージ自体も、6.0.0よりヘルスコネクトに対応しており、HealthFactoryクラスのuseHealthConnectIfAvailable引数で制御可能です。権限に関する記述もpub.devに示されているのでご参考ください。バージョン10.0.0以上の場合はインスタンスの初期化(configreの引数)、それ以前はHealthFactoryのコンストラクタ引数で指定します。
Dart
#バージョン10.0.0以降
void initState() {
// configure the health plugin before use.
Health().configure(useHealthConnectIfAvailable: true); //これ。useHealthConnectIfAvailable引数はアプリの方針でオンオフを指定してください。
super.initState();
}
Dart
#バージョン9.0.0以前(旧バージョン)
final health = HealthFactory(useHealthConnectIfAvailable: true);
尚、ヘルスコネクトとのアクセスには、これまでのGoogle Fitの申請とは別の方法がとられており、こちらのフォーム(Google Health Connect API Request)に入力する必要があります。こちらの審査に受かっていないと、「このアプリはヘルスコネクトにアクセスできません。」と表示されブロックされます。
この申請においてもアプリに対する審査、特にアプリに指定しているプライバシーポリシーに対する審査が行われます。ヘルスコネクトのポリシー要件に関するよくある質問 | Play Consoleヘルプに詳しいポリシーが記載されています。
healthパッケージについて、ご相談承ります
諸々難解なHealthパッケージについて、解決できない部分のご相談や実装実務を提供いたします。
弊社の中核ノウハウでもあり相応のコストをお申し受けしますが、ご相談は下にあるお問い合わせボタンからご連絡ください。
記事筆者へのお問い合わせ、仕事のご依頼
当社では、IT活用をはじめ、業務効率化やM&A、管理会計など幅広い分野でコンサルティング事業・IT開発事業を行っております。
この記事をご覧になり、もし相談してみたい点などがあれば、ぜひ問い合わせフォームまでご連絡ください。
皆様のご投稿をお待ちしております。