いつも通り、healthパッケージを導入して、Readmeのドキュメントの通りに作業を進めることにします。Setupのセクションを見ながら進めます。手順を以下に記載していきます。
Apple Health (iOS)
まず、iOSの方から設定していきます。
Step1:ios/Runner/Info.plistを開いて、2つのエントリーを追加し、右上の「Open iOS module in Xcode」をクリックします。
Step2:Xcodeが開くので、「Signing&Capabilities」を開いて、Allタブの左にある「+」をクリックします。
Capabilities(機能)を選ぶダイアログが表示されるので、左ペインから「HealthKit」をダブルクリックするか、ドラッグ&ドロップします。
Xcodeの「Signing&Capabilities」の一番下に「HealthKit」が追加されます。
Google Fit (Android)
次にAndroidの方を設定していきます。
Androidの設定は、Google Cloud Platform(以下GCP)にプロジェクトを作って、そこにフィットネスの権限を設定し、接続するようです。接続の際に、開発者を限定というか制限するために、フィンガープリントと言うSHA-1ハッシュ値を設定します。
まず、ターミナルで、キーストアディレクトリ(ホームディレクトリ内の.androidディレクトリのこと)に移動してみるのですが、お目当ての「debug.keystore」ファイルが見当たりませんでした。
ダメ元で、指定されたコマンドを実行してみますが、「キーストア・ファイルは存在しません」と言われ、やっぱりダメでした。
ドキュメントに記載されているガイドを見てみると、「A debug certificate: The Android SDK tools generate this certificate automatically when you do a debug build. 」と言う記述がありました。要するにデバッグ実行をすると、勝手に作成されるよーとのこと。
言われてみれば、ずっとiOSシミュレーターばかりでデバッグ実行していて、Androidでの動作確認はしていませんでした。
ということで、アプリの実行ターゲットを「Open Android Emulator」を選んで、Androidのエミュレーターを呼び出しました。エミュレートにパワーがいるのか、実行されるまですごい時間がかかりました。測ってはいませんが、30分くらい?放っておいてお昼ご飯いただきましたw
下記のようなダイアログが出ました。
Could not automatically detect an ADB binary. Some emulator functionality will not work until a custom path to ADB is added. This can be done in Extended Controls (...) > Settings > General tab > 'Use detected ADB location'
日本語訳すると以下のように書いています。
ADBバイナリを自動的に検出できませんでした。ADBへのカスタムパスが追加されるまで、一部のエミュレータ機能は動作しません。これは、Extended Controls (…) > Settings > General tab > 'Use detected ADB location' で行うことができます。
と言われたものの、Androidエミュレーターは頑張って動き出そうとしていたので、放っておきました。
Androidエミュレーターが起動してアプリが動いてから、先ほどのキーストアディレクトリの中身を確認してみると、キタ━━(゚∀゚)━━!!!!
debug.keystoreファイルが出来ました。
これで指示通りのkeytoolコマンドを実行することが出来ます。
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
GCPのプロジェクトを作成し、フィットネスAPIへのアクセスを有効化します。
有効なAPIにフィットネスAPIが含まれていることを確認します。
次にOAuthクライアントIDを作成します。
ここに先ほど作成した、フィンガープリントを登録します。
OAuthクライアントを作成すると、クライアントIDが表示されます。
この示されたクライアントIDは特にソースのどこに書き込めば良いとかの情報がないので、そのままAndroid 10(Xperia 8 Lite)実機で実行してみましたが、認証エラーでヘルスデータの取得ができませんでした。この件については、引き続き調査しようと思います。
ヘルスデータをAPIを通じて取得するのであれば、Android端末のヘルスデータをクラウドに上げておくような設定が必要なんじゃないかなぁ…その辺りがよくわからないです。
実行結果
healthパッケージの例をそのまま貼り付けしたコードで、iOSでは正常に実行出来ました。
例のコードが少し古いためか、Warningが出ていたところを出来る限り修正したのと、コメントに英語で説明されていたところを日本語化して実行しました。一部コメントを追加しているところもあります。
import 'dart:async';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:health/health.dart';
void main() => runApp(const HealthApp());
class HealthApp extends StatefulWidget {
const HealthApp({Key? key}) : super(key: key);
@override
_HealthAppState createState() => _HealthAppState();
}
enum AppState {
DATA_NOT_FETCHED,
FETCHING_DATA,
DATA_READY,
NO_DATA,
AUTH_NOT_GRANTED,
DATA_ADDED,
DATA_NOT_ADDED,
STEPS_READY,
}
class _HealthAppState extends State<HealthApp> {
List<HealthDataPoint> _healthDataList = [];
AppState _state = AppState.DATA_NOT_FETCHED;
int _nofSteps = 10;
double _mgdl = 10.0;
// アプリで使用するHealthFactoryを作成する
HealthFactory health = HealthFactory();
/// healthプラグインからデータポイントを取得し、アプリに表示する
Future fetchData() async {
setState(() => _state = AppState.FETCHING_DATA);
// 取得する型を定義する
final types = [
HealthDataType.STEPS,
HealthDataType.WEIGHT,
HealthDataType.HEIGHT,
HealthDataType.BLOOD_GLUCOSE,
// iOSではこの行をコメント解除してください - iOSでのみ利用可能
HealthDataType.DISTANCE_WALKING_RUNNING,
];
// パーミッション対応
final permissions = [
HealthDataAccess.READ,
HealthDataAccess.READ,
HealthDataAccess.READ,
HealthDataAccess.READ,
// typesの要素数と合わせる必要があるため、1つ追加
HealthDataAccess.READ,
];
// 過去24時間以内のデータを取得する
final now = DateTime.now();
final yesterday = now.subtract(const Duration(days: 1));
// データ型へのアクセスを要求する
// READアクセスのみ必要なので、厳密にはpermissionsは必要ない
bool requested = await health.requestAuthorization(types, permissions: permissions);
if (requested) {
try {
// ヘルスデータを取得する
List<HealthDataPoint> healthData =
await health.getHealthDataFromTypes(yesterday, now, types);
// 新しいデータポイントをすべて保存する(最初の100件のみ)
_healthDataList.addAll((healthData.length < 100)
? healthData
: healthData.sublist(0, 100));
} catch (error) {
if (kDebugMode) {
print("Exception in getHealthDataFromTypes: $error");
}
}
// 重複データは除外する
_healthDataList = HealthFactory.removeDuplicates(_healthDataList);
// 結果をスナップ出力する
for (var x in _healthDataList) {
if (kDebugMode) {
print(x);
}
}
// 結果を表示するために、UIを更新する
setState(() {
_state =
_healthDataList.isEmpty ? AppState.NO_DATA : AppState.DATA_READY;
});
} else {
if (kDebugMode) {
print("認証でNG");
}
setState(() => _state = AppState.DATA_NOT_FETCHED);
}
}
/// ランダムなhealthデータを追加する
Future addData() async {
final now = DateTime.now();
final earlier = now.subtract(const Duration(minutes: 5));
_nofSteps = Random().nextInt(10);
final types = [HealthDataType.STEPS, HealthDataType.BLOOD_GLUCOSE];
final rights = [HealthDataAccess.WRITE, HealthDataAccess.WRITE];
final permissions = [
HealthDataAccess.READ_WRITE,
HealthDataAccess.READ_WRITE
];
bool? hasPermissions =
await HealthFactory.hasPermissions(types, permissions: rights);
if (hasPermissions == false) {
await health.requestAuthorization(types, permissions: permissions);
}
_mgdl = Random().nextInt(10) * 1.0;
bool success = await health.writeHealthData(
_nofSteps.toDouble(), HealthDataType.STEPS, earlier, now);
if (success) {
success = await health.writeHealthData(
_mgdl, HealthDataType.BLOOD_GLUCOSE, now, now);
}
setState(() {
_state = success ? AppState.DATA_ADDED : AppState.DATA_NOT_ADDED;
});
}
/// ヘルスプラグインから歩数を取得し、アプリに表示する
Future fetchStepData() async {
int? steps;
// 本日(要するに午前0時から)の歩数を取得する
final now = DateTime.now();
final midnight = DateTime(now.year, now.month, now.day);
bool requested = await health.requestAuthorization([HealthDataType.STEPS]);
if (requested) {
try {
steps = await health.getTotalStepsInInterval(midnight, now);
} catch (error) {
if (kDebugMode) {
print("Caught exception in getTotalStepsInInterval: $error");
}
}
if (kDebugMode) {
print('Total number of steps: $steps');
}
setState(() {
_nofSteps = (steps == null) ? 0 : steps;
_state = (steps == null) ? AppState.NO_DATA : AppState.STEPS_READY;
});
} else {
if (kDebugMode) {
print("Authorization not granted");
}
setState(() => _state = AppState.DATA_NOT_FETCHED);
}
}
Widget _contentFetchingData() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: const EdgeInsets.all(20),
child: const CircularProgressIndicator(
strokeWidth: 10,
)),
const Text('データ取得中...')
],
);
}
Widget _contentDataReady() {
return ListView.builder(
itemCount: _healthDataList.length,
itemBuilder: (_, index) {
HealthDataPoint p = _healthDataList[index];
return ListTile(
title: Text("${p.typeString}: ${p.value}"),
trailing: Text('${p.unitString} '),
subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
);
});
}
Widget _contentNoData() {
return const Text('No Data to show');
}
Widget _contentNotFetched() {
return Column(
children: const [
Text('ダウンロードボタンを押すと、データを取得する'),
Text('プラスボタンを押すと、ランダムなヘルスデータを追加する'),
Text('ウォーキングボタンを押すと、合計歩数を取得する'),
],
mainAxisAlignment: MainAxisAlignment.center,
);
}
Widget _authorizationNotGranted() {
return const Text('権限が与えられていない'
'Androidの場合は、Google Developer ConsoleでOAUTH2クライアントIDが正しいかどうか確認してください'
'iOSの場合は、Apple Healthのアクセス権を確認してください');
}
Widget _dataAdded() {
return Text('$_nofSteps 歩、$_mgdl mgdlは正常に挿入された');
}
Widget _stepsFetched() {
return Text('合計歩数: $_nofSteps');
}
Widget _dataNotAdded() {
return const Text('データの追加に失敗');
}
Widget _content() {
switch (_state) {
case AppState.DATA_READY:
return _contentDataReady();
case AppState.NO_DATA:
return _contentNoData();
case AppState.FETCHING_DATA:
return _contentFetchingData();
case AppState.AUTH_NOT_GRANTED:
return _authorizationNotGranted();
case AppState.DATA_ADDED:
return _dataAdded();
case AppState.STEPS_READY:
return _stepsFetched();
case AppState.DATA_NOT_ADDED:
return _dataNotAdded();
default:
break;
}
return _contentNotFetched();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Health Example'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.file_download),
onPressed: () {
fetchData();
},
),
IconButton(
onPressed: () {
addData();
},
icon: const Icon(Icons.add),
),
IconButton(
onPressed: () {
fetchStepData();
},
icon: const Icon(Icons.nordic_walking),
)
],
),
body: Center(
child: _content(),
)),
);
}
}