歩数、距離、カロリー、心拍数、睡眠は、ヘルスケアおよび栄養管理アプリによってユーザーのプラットフォーム(Android端末の Android Health Connect、iOS端末の Apple HealthKit)から取得され、整理され、重複排除された単一のデイリープロフィールとして表示される必要があります。これら2つの API 間には類似点がほとんどありません。読み取りメカニズムの相違、異なる認証スキーム、および異なるデータ構造です。私たちは、これらをあたかも1つのもののように見せるモバイル同期レイヤーを構築しました。
課題
- 何一つ一致しない2つのネイティブ API。 Apple HealthKit はコールバックを通じて日次集計を返します。Health Connect は、豊富なソースメタデータを含む生のページネーションされたサンプルを返します。概念は同じでも、形状は全く異なります。そしてアプリは両方を1つのスキーマに正規化する必要がありました。
- デフォルトでは二重カウントが発生。 電話、スマートウォッチ、およびサードパーティ製アプリはすべて、同じ8,000歩を報告する可能性があります。単純に合計すると、すべての指標が膨らんでしまいます。アプリは重複するソースを認識し、それぞれの実世界の活動を一度だけカウントする必要がありました。
- 鮮度を維持するためにバッテリーやネットワークを犠牲にすることはできません。 健康データは一日中変化しますが、同期ごとに全履歴を読み取ると、バッテリーを消耗し、セルラー接続を飽和させてしまいます。同期頻度は、リアルタイム性を維持しつつ軽量である必要がありました。
権限は複雑になりがちです。 数十種類のデータ型、2つの権限モデル、バックグラウンド読み取り制限、そして一部のスコープは許可するが、他のスコープは許可しないユーザー — これらすべてを、アプリはフローをクラッシュさせることなく処理しなければなりません。
私たちのソリューション
私たちは**3層同期**を構築しました。薄いネイティブレイヤーが各プラットフォームの API を読み取り、**オンデバイス正規化レイヤー**がデータを重複排除し、日次集計にまとめ、バックエンドはそれを保存し、単一の信頼できる台帳に調整します。クライアントは生のサンプルを送信せず、クリーンでタイムゾーン補正された日次集計を送信します。

アーキテクチャ
- iOS は Apple HealthKit を react-native-health を通じて読み取ります。Android は Health Connect を react-native-health-connect を通じて読み取ります。
- オンデバイス正規化は、両プラットフォームの出力を単一のスキーマに変換し、データが電話から離れる前に重複を排除します。
- 同期トリガー — 初回起動時に30日間の履歴を読み込み、その後は10分間隔およびアプリ再開ごとに軽量な1日分の増分読み取りを行います。
- 転送 — 正規化された日次集計は、メインサーバーの /user-health/system-activity に POST され、これがそれらを(ユーザーID + タイムゾーンとともに)ヘルスケアマイクロサービスに転送します。
- 永続化 — 各ソース/日は HealthSystemAggregate として冪等に保存されます。その後、MongoDB change stream がそれを日ごとの HealthLedger に調整します。
ネイティブの HealthKit および Health Connect モジュールを備えた Expo / React Native 管理ワークフロー。
主要機能
- 2つの API に対する単一の正規化契約。 プラットフォーム固有のリーダーが単一の formatHealthData ステップにデータを供給します。これはソースに関係なく同じ形式を出力するため、バックエンドとUIはiOSとAndroidで分岐することはありません。
2. デバイス上での最大値ベースの重複排除。 各ソース内で、1日の読み取り値は合計されます。その後、日次値はソース「全体」の Math.max() となります(カロリーは source|deviceType でキー付けされます)。これにより、1つのデバイスからの複数の読み取り値は累積されますが、同じ日を報告するウォッチと電話が二重カウントされることはありません:
// Sum readings within each source, then take the max ACROSS sources dateSourceMap[date][source] += entry.count; const totalSteps = Math.ceil(Math.max(...Object.values(dateSourceMap[date]))); |
3.プラットフォームごとの適切な読み取り戦略。 Health Connect の読み取りは、pageToken ループ(1ページあたり1,000レコード)でページネーションされます。HealthKit の日次集計は日ごとにループされます。各特異性は独自のリーダー内にカプセル化されており、アプリの他の部分からは見えません。
4. バッテリーを考慮した同期頻度。 30日間の完全なバックフィルは一度だけ実行され(初回呼び出しフラグによって保護されます)、その後は、各同期は1日分のみを取得します — セルラー通信では高速、電力消費も低く抑えられます。
5. タイムゾーン補正された日次グループ化。 サンプルは、ユーザー自身のタイムゾーンで YYYY-MM-DD キーにバケット化されます。そのため、午後11時のワークアウトは、サーバーがどこにあっても正しい日に記録されます。
6. 冪等なバックエンド書き込み。 各ロールアップは (userId, deviceId, source, date) で upsert されます — 同じ日を再送信しても何もしないため、リトライや重複する同期が安全になります。
7. 健全な劣化処理。 権限の欠如、空の読み取り、Health Connect のレート制限エラーは捕捉され、きれいに表面化されます — 空の日は同期をクラッシュさせることはなく、ユーザーにはサイレントな失敗の代わりに明確なプロンプトが表示されます。
結果
- iOS と Android を横断する単一で一貫性のあるヘルスケアプロフィール — プラットフォームの違いはアプリの他の部分からは見えません。
- 重複するデバイス/アプリソースは重複排除され、歩数とカロリーは誇張された合計ではなく、現実を反映します。
- 軽量な増分同期により、バッテリーを消耗したりモバイルデータを使い果たしたりすることなく、データを新鮮に保ちます。
タイムゾーン補正され、冪等な書き込みにより、すべての指標が正しい日に記録され、リトライによってレコードが破損することはありません。
技術スタック
React Native (Expo) · react-native-health (HealthKit) · react-native-health-connect · NestJS · MongoDB · MongoDB Change Streams · moment-timezone

