本文へスキップ
すべての記事

2026年のAndroid Health Connect:ローカルファーストを手放さずにデータを読み書きする

AndroidのHealth Connect APIに関する2026年の実践ガイド — 権限、バックグラウンド読み込み、そしてローカルファーストなアプリがそれを「真実」ではなく「オプション」として扱うべき理由について。

MFKAPPS 1 分で読めます

Health Connectは、Androidが今や健康・フィットネス系アプリに求める端末上のストアだ。独自のサイロを抱え込む代わりに、ここから読み書きすることが前提になっている。ローカルファーストなアプリにとって、これは正直な問いを突きつけてくる。ここはデータを公開する場所なのか、読み取る場所なのか、それとも両方なのか——そしてどちらかにイエスと答えた瞬間、自分が約束した「データの真実はこの端末にある」という前提が、静かに端末の外へずれていくのではないか。このAPIが実際に何を要求してくるのか、そして私がどこに線を引いているかを書いていく。

Health Connectとは何か、何ではないか

Health Connectはシステムレベルのデータストアであって、クラウドサービスではない。歩数、水分摂取量、体重、睡眠セッションといったレコードは端末内のローカルデータベースに存在し、Androidがそのアクセスを仲介する。ユーザーが承認したアプリなら、どれでもそこを通じて読み書きできる。ここに魅力がある——水分記録アプリとフィットネスアプリが、それぞれ別々に不完全な数字を抱えるのではなく、一つの水分摂取合計を共有できる。だが同時にリスクでもある。自分のアプリがそこに書き込んだ瞬間、ユーザーが読み取り権限を許可した他のどのアプリからも、そのデータが見えるようになる。Health Connectに触れたからといってローカルファーストという言葉が意味を失うわけではないが、その意味はより狭くなる——データは依然として端末の外へは出ないが、もはや自分だけに見えているわけではない。

Health Connectアプリがプリインストールされていない端末では、初めてリクエストした際にクライアントライブラリがPlayストアからのインストールを促す。この失敗経路には予算を割いておくべきだ。新品の端末や機能を削られたROMではAPIが利用できないことがあり、権限リクエストにはクラッシュではない本物のフォールバックが必要になる。

2026年の権限モデル

Healthの権限は、ActivityCompat.requestPermissions的な意味でのランタイム権限ではない。自分のアプリのダイアログではなく、Health Connectアプリが管理する専用の説明画面を経由する。マニフェストで意図を宣言し、コントラクトを起動する形だ。

val requestPermissions = registerForActivityResult(
    PermissionController.createRequestPermissionResultContract()
) { granted ->
    if (HealthPermission.getWritePermission(HydrationRecord::class) in granted) {
        // proceed
    }
}

requestPermissions.launch(
    setOf(
        HealthPermission.getWritePermission(HydrationRecord::class),
        HealthPermission.getReadPermission(HydrationRecord::class),
    )
)

返ってくる結果セットは何が許可されたかしか教えてくれず、なぜ拒否されたかは決して教えてくれない——「二度と表示しない」フラグもなければ、確認できる理由もない。期待していた権限が返却セットに含まれていなかった場合、正しい対応は「三画面前に得た許可がまだ有効だ」と決めつけることではなく、読み書きのたびにPermissionController.getGrantedPermissions()を確認して、優雅に機能を縮退させることだ。ユーザーはいつでもシステム設定からHealth Connectの権限を取り消せる。それは完全にアプリのライフサイクルの外側で起きるので、次に呼んだAPIは、まるで一度も許可されたことがなかったかのようにただ失敗する。

レコードの書き込みと読み込み

HydrationRecordは、時間範囲と量を持つ値で、書き込んだアプリ自身のメタデータに紐づいている。

val record = HydrationRecord(
    startTime = Instant.now(),
    startZoneOffset = ZoneOffset.systemDefault().rules.getOffset(Instant.now()),
    endTime = Instant.now(),
    endZoneOffset = ZoneOffset.systemDefault().rules.getOffset(Instant.now()),
    volume = Volume.milliliters(250.0),
)

healthConnectClient.insertRecords(listOf(record))

読み込みは全件スキャンではなく時間範囲クエリだ。「すべて」ではなく、一定のウィンドウを指定することが求められる。

val response = healthConnectClient.readRecords(
    ReadRecordsRequest(
        recordType = HydrationRecord::class,
        timeRangeFilter = TimeRangeFilter.between(
            startOfToday, Instant.now()
        ),
    )
)

すべてのレコードはmetadata.dataOriginを持っているので、自分の書き込みとフィットネストラッカーや他のアプリの書き込みを区別できる。複数のソースから「今日の合計」を集計していて、二重カウントを避けたいときに役立つ。

バックグラウンド読み込みには別の権限がいる

アプリがフォアグラウンドにない間にHealth Connectのデータを読み込むには、レコードごとの権限に加えてPERMISSION_READ_HEALTH_DATA_IN_BACKGROUNDが必要で、Androidはこれを説明画面に専用の一行を設けるほどセンシティブなものとして扱う。ユースケースが「アプリを開いたときに一度だけ同期する」というものなら、これは要求しない方がいい。ウィジェットや、ユーザーがアプリを開く前に数字を計算しておく必要がある日次サマリー通知のような、本物のバックグラウンドジョブのためにだけリクエストすること。不要なのにリクエストしても技術的に失敗するわけではないが、その機能に見合わないほど権限画面を長く、物々しくしてしまう——それはやがて許可そのものを失う類の失敗だ。

なぜオプションの追加データとして扱うのか

Hydrameのようなアプリにとって、Health Connectは一つの理由で魅力的だ。他の場所でもワークアウトを記録しているユーザーが、食い違う二つの数字の代わりに、一つの正しい水分摂取量を得られる。だが同時に、間違った理由でも魅力的に見えてしまう——「Health Connectに同期する」がいつの間にか「Health Connectこそが真実のソースになる」へとすり替わるのは簡単で、そうなるとアプリ自身のローカルデータベースはずれていく可能性のあるキャッシュへと変わり、「何も端末の外に出ない」という約束は、読み取り権限を許可するアプリが増えるたびにあいまいになっていく。私が実際に出荷したいバージョンは、Health Connectを同じローカルレコードの一方向の公開先として扱うものであり、アプリが機能するために依存する読み取り経路には決してしない——Health Connectアプリを丸ごとアンインストールしても、コア体験には何の影響もないはずだ。

チェックリスト

Health Connectを組み込む前に確認すること。可用性をチェックし、アプリが存在しないケースを処理する。実際に使うレコード権限だけをリクエストする。三画面前の許可をキャッシュするのではなく、アクセスのたびに許可された権限を再確認する。バックグラウンド読み込みは本物のバックグラウンド作業だけに絞る。そして、Health Connectに公開するのか、それに依存するのかを事前に決めておく——このAPIはどちらもあっさりやらせてくれるが、ローカルファーストなアプリがデータの実際の在り処について誠実でいられるのは、そのうちの一方だけだ。