はじめに
少し前から高血圧の薬を飲み始めました。
そのため、毎日朝晩の2回、血圧と脈拍を測定しています。
私は普段からNotionをよく使っているので、測定した血圧と脈拍もNotionのデータベースに記録していました。
一方で、運動不足も気になっていたため、少し意識して歩くようにもしていました。
歩数はAndroidスマホに自動的に蓄積されています。
ただし、データの保存先は分かれていました。
| データ | 保存先 |
|---|---|
| 血圧・脈拍 | Notion |
| 歩数 | Androidスマホ |
血圧、脈拍、歩数を一つの場所で確認できれば、日々の健康管理がもう少し分かりやすくなるはずです。
そこで、AndroidスマホとNotionの間で健康データを同期するアプリを作りました。
作ったもの
今回作成したアプリは、Health ConnectとNotionを連携するAndroidアプリです。
リポジトリはこちらです。
アプリ名は Health Notion Sync です。
主な役割は次の3つです。
- スマホに蓄積された歩数をNotionへ同期する
- Notionに保存した血圧・脈拍をHealth Connectへ同期する
- 血圧・脈拍をアプリから簡単にNotionへ登録する
データの流れは次のようになります。
歩数は スマホからNotionへ 同期します。
血圧と脈拍は Notionからスマホへ 同期します。
血圧・脈拍については、アプリからNotionへ新規登録することもできます。
使用技術
今回のアプリでは、以下の技術を使用しています。
| 用途 | 技術 |
|---|---|
| Androidアプリ | Kotlin |
| 健康データ連携 | AndroidX Health Connect Client |
| バックグラウンド処理 | AndroidX WorkManager |
| 非同期処理 | Kotlin Coroutines |
| 外部データ保存 | Notion API |
| 音声入力 | Android RecognizerIntent
|
| トークン保護 | Android Keystore |
| APK配布 | GitHub Actions / GitHub Releases |
大規模なバックエンドはありません。
AndroidアプリからHealth ConnectとNotion APIへ直接アクセスする構成です。
Health Connectとは
Health Connectは、Android端末上で健康データを共有するための仕組みです。
歩数、心拍、血圧などのデータを、対応するアプリ間で連携できます。
今回のアプリでは、次のレコードを扱っています。
StepsRecord
BloodPressureRecord
HeartRateRecord
歩数はHealth Connectから読み取り、血圧と心拍はHealth Connectへ書き込みます。
必要な権限もアプリ側で明示的に要求します。
private val requiredPermissions = setOf(
HealthPermission.getReadPermission(StepsRecord::class),
HealthPermission.getReadPermission(BloodPressureRecord::class),
HealthPermission.getWritePermission(BloodPressureRecord::class),
HealthPermission.getReadPermission(HeartRateRecord::class),
HealthPermission.getWritePermission(HeartRateRecord::class),
HealthPermission.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND
)
ユーザーが許可した健康データだけを読み書きします。
歩数をNotionへ同期する
Health Connectには、細かい時間単位で歩数レコードが保存されています。
そのままNotionへ送ると行数が増えすぎるため、アプリ側で1日単位に集計しています。
同期の流れは次の通りです。
実装では、Health Connectの集計APIを使用しています。
val aggregatedByDay = client.aggregateGroupByPeriod(
AggregateGroupByPeriodRequest(
metrics = setOf(StepsRecord.COUNT_TOTAL),
timeRangeFilter = aggregateTimeRange,
timeRangeSlicer = Period.ofDays(1),
dataOriginFilter = GOOGLE_FIT_DATA_ORIGIN_FILTER
)
)
Notion側では、1日につき1行だけ保持します。
同じ日のデータがすでに存在する場合は、新しいレコードが追加されるたびに行を更新します。
これにより、当日分の歩数も途中経過を含めて同期できます。
Notionの歩数データベースには、最低限以下のプロパティが必要です。
| プロパティ | 型 | 既定名 |
|---|---|---|
| 日時 | Date | 日付 |
| 歩数 | Number | 歩数 |
血圧と脈拍をHealth Connectへ同期する
血圧と脈拍は、逆方向に同期します。
Notionに保存しているデータを取得し、Health Connectへ書き込みます。

血圧と脈拍は、それぞれHealth Connectのレコードに変換します。
BloodPressureRecord(
time = measuredAt,
zoneOffset = zoneOffset,
metadata = Metadata.manualEntry("notion-blood-pressure-$measuredAt"),
systolic = Pressure.millimetersOfMercury(systolic),
diastolic = Pressure.millimetersOfMercury(diastolic)
)
HeartRateRecord(
startTime = measuredAt,
startZoneOffset = zoneOffset,
endTime = measuredAt.plusSeconds(1),
endZoneOffset = zoneOffset,
samples = listOf(
HeartRateRecord.Sample(measuredAt, heartRate)
),
metadata = Metadata.manualEntry("notion-heart-rate-$measuredAt")
)
Notionのバイタルデータベースには、以下のプロパティが必要です。
| プロパティ | 型 | 既定名 |
|---|---|---|
| 測定日時 | Date | Date |
| 最高血圧 | Number | 収縮期 |
| 最低血圧 | Number | 拡張期 |
| 脈拍 | Number | 脈拍 |
プロパティ名はアプリの設定画面から変更できます。
Health Connectへ同期した血圧と脈拍は、Google FitまたはGoogle Healthなどの対応アプリから確認できます。
スマホ側では、血圧の推移がグラフ化されるため、Notionを開かなくても変化をすぐに確認できます。

音声入力で血圧・脈拍を登録する
毎日朝晩の2回、血圧と脈拍を入力するのは意外と面倒です。
入力の手間を減らすため、アプリからNotionへ直接登録できるようにしました。
さらに、Androidの音声認識機能を使って、3項目をまとめて入力できます。
例えば、次のように話します。
128、82、71
すると、以下の入力欄へ自動的に反映されます。
| 入力欄 | 値 |
|---|---|
| 最高血圧 | 128 |
| 最低血圧 | 82 |
| 脈拍 | 71 |
項目名を話す必要はありません。
血圧計を見ながら3つの数字だけを順番に読み上げればよいので、毎日の入力がかなり楽になりました。
音声認識の起動にはAndroidの RecognizerIntent を利用しています。
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
)
putExtra(
RecognizerIntent.EXTRA_LANGUAGE,
Locale.JAPAN.toLanguageTag()
)
putExtra(
RecognizerIntent.EXTRA_PROMPT,
"最高血圧、最低血圧、脈拍の順に数字を話してください。"
)
putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 3)
}
音声認識結果から数字を抽出し、3項目へ順番に割り当てます。
単純な数字抽出だけでなく、認識結果が連結された場合もある程度補正します。
例えば、3つの数字が一続きになってしまった場合は、最高血圧、最低血圧、脈拍として妥当そうな組み合わせを選びます。
判定には、おおよそ以下の範囲を利用しています。
| 項目 | 想定範囲 |
|---|---|
| 最高血圧 | 80〜250 |
| 最低血圧 | 40〜150 |
| 脈拍 | 40〜220 |
加えて、最高血圧が最低血圧以下になった場合は、大きなペナルティを与えます。
if (systolic <= diastolic) {
penalty += 1_000.0 + (diastolic - systolic)
}
音声認識が完全でなくても、実用上使いやすくするための補正です。
手動同期と自動同期
TOP画面には、次の3つの同期ボタンがあります。
| ボタン | 処理 |
|---|---|
歩数 |
スマホの歩数をNotionへ同期 |
血圧・脈拍 |
Notionの血圧・脈拍をHealth Connectへ同期 |
すべて |
両方をまとめて同期 |
同期対象は直近30日間です。
TOP画面では、スマホ側とNotion側の最新データ日時も確認できます。
また、WorkManagerを利用して、1日1回の自動同期も実装しています。
val request = PeriodicWorkRequestBuilder<AutoSyncWorker>(
1,
TimeUnit.DAYS
)
.setInitialDelay(
initialAutoSyncDelayMillis(autoSyncTime),
TimeUnit.MILLISECONDS
)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
ネットワーク接続がある場合に実行されます。
自動同期の最終成功時刻、最終失敗時刻、失敗理由、次回予定時刻も画面から確認できます。
設定画面
設定画面では、以下を入力します。
- Notion Integration Token
- 歩数データベースのData Source ID
- 歩数データベースのプロパティ名
- 血圧・脈拍データベースのData Source ID
- 血圧・脈拍データベースのプロパティ名
- 自動同期時刻
- Health Connectの権限
注意点として、設定するのはNotionの Database IDではなくData Source ID です。
また、Notion側では対象のデータベースをIntegrationへ共有しておく必要があります。
トークンは端末内で暗号化して保存する
Notion Integration Tokenを平文で保存するのは避けたいところです。
このアプリでは、Android Keystoreを利用して端末内で暗号化して保存しています。
また、Notion Integrationには必要最小限のデータベースだけを共有します。
個人用アプリでも、健康データを扱う以上、最低限の権限管理は必要です。
GitHub ActionsでAPKを自動リリースする
main ブランチへ変更を反映すると、GitHub Actionsで署名済みAPKをビルドし、GitHub Releaseへ添付します。
APKファイル名にはバージョン番号を含めています。
sync-health-notion-v0.0.8-release.apk
Androidの versionCode は、SemVerから生成しています。
val versionCode = major * 10_000 + minor * 100 + patch
例えば 0.0.8 の場合、versionCode は 8 です。
この仕組みにより、新しいAPKを端末へ上書きインストールできます。
作って良かったこと
このアプリを作って一番良かったのは、健康管理のデータがつながったことです。
Notionでは、今まで記録していた血圧と脈拍に加えて、歩数もグラフ化できるようになりました。
歩数グラフを見ると、
「今日は少し歩数が少ない」
「最近は良い感じに歩けている」
という変化がすぐに分かります。
良い状態を維持したいと思うので、以前より歩くことに前向きになりました。
血圧と脈拍もHealth Connectへ同期できるため、Google FitまたはGoogle Healthからすぐにグラフを確認できます。
数値の変化が見えると、健康管理を続けるモチベーションになります。
おわりに
今回作ったものは、健康管理を劇的に変える大規模なサービスではありません。
毎日少し面倒だった入力作業を減らし、別々の場所に保存されていたデータをつないだ、小さなAndroidアプリです。
ただ、自分自身が毎日使うものなので、効果は大きく感じています。
エンジニアとして便利なものを作るだけでなく、自分の生活を少し改善するためにコードを書くのも良いものです。
今後も実際に使いながら、必要な機能を追加していく予定です。






