Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card
Eine Gesundheits-App mit Flutter entwickeln
Tutorial: Eine Flutter-App, die sich mit der FHIR R4 Akte einer Person über OAuth Consent und PKCE verbindet, Tokens sicher speichert, Blutdruckwerte mit dem Dart SDK liest und hinzufügt und widerrufene Zugriffe handhabt.
Dieses Tutorial erstellt eine Flutter-App, die sich mit dem HL7 FHIR R4-Medizindatensatz einer Person mit deren Zustimmung verbindet, deren Blutdruckwerte anzeigt und neue hinzufügt, unter Verwendung des Dart SDK anpheros_sdk. Die App kommuniziert direkt mit Anpheros über ein vom Benutzer erteiltes OAuth-Token, nicht mit einem API-Schlüssel, sodass sie kein eigenes Backend für die medizinischen Daten benötigt. Sie läuft unter iOS und Android; derselbe Code funktioniert unter macOS und im Web mit der Plattformkonfiguration des Auth-Pakets.
Wie die Teile zusammenpassen
┌──────────────────────┐ 1. "Connect" opens the consent screen ┌───────────────────────────┐
│ Flutter app │ ─────────────────────────────────────► │ Anpheros │
│ anpheros_sdk │ ◄───────────────────────────────────── │ sign-in + consent screen │
│ flutter_web_auth_2 │ 2. bpapp://callback?code=… │ │
│ flutter_secure_storage│ 3. code + PKCE verifier → tokens │ OAuth 2.1 / SMART on FHIR │
│ │ 4. read / write within the scopes │ FHIR R4 record, audit │
└──────────────────────┘ ─────────────────────────────────────► └───────────────────────────┘
Eine mobile App darf niemals einen API-Schlüssel enthalten: Jeder kann ihn extrahieren, und ein Schlüssel erreicht jeden Patienten Ihres Projekts. Mit OAuth erreicht der Token in der App einen Datensatz, nur das, was die Person genehmigt hat, bis sie ihn widerruft. Der Ablauf ist der Standard-SMART-on-FHIR-Standalone-Launch, wie in How to implement patient consent with SMART on FHIR beschrieben; das SDK erledigt die Protokollarbeit.
Bevor Sie beginnen
- Flutter 3.22 oder neuer.
- Ein Anpheros-Projekt. Melden Sie sich unter platform.anpheros.com/dashboard mit Google an und erhalten Sie einen Sandbox-Schlüssel; das Projekt wird damit geliefert.
- Registrieren Sie im Dashboard eine Anwendung vom Typ public mit der Redirect URI
bpapp://callback. Notieren Sie sich derenclient_id.
Während der privaten Beta verwenden OAuth-Verbindungen Sandbox-Datensätze, sodass Sie den gesamten Ablauf ohne reale Daten testen können.
flutter create bp_app && cd bp_app
flutter pub add anpheros_sdk flutter_web_auth_2 flutter_secure_storage
Android: Fügen Sie die Callback-Aktivität von flutter_web_auth_2 in <application> in android/app/src/main/AndroidManifest.xml ein, mit Ihrem Schema:
<activity android:name="com.linusu.flutter_web_auth_2.CallbackActivity" android:exported="true" android:taskAffinity="">
<intent-filter android:label="flutter_web_auth_2">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bpapp" />
</intent-filter>
</activity>
iOS benötigt keine zusätzliche Einrichtung für ein benutzerdefiniertes Schema.
Schritt 1: Verbindung zum Datensatz herstellen
OAuthFlow erstellt die Autorisierungsadresse mit einer PKCE-Herausforderung; flutter_web_auth_2 öffnet diese in einem sicheren Browser-Tab und kehrt zurück, wenn Anpheros zu bpapp://callback weiterleitet.
// lib/record_connection.dart
import 'dart:convert';
import 'dart:math';
import 'package:anpheros_sdk/anpheros_sdk.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
const _base = 'https://platform.anpheros.com';
const _clientId = 'client_…'; // from the dashboard
const _scopes = [
'patient/Observation.rs?category=vital-signs', // read vital signs only
'patient/Observation.c', // add readings
'offline_access', // stay connected without asking again every 30 minutes
];
class RecordConnection {
final _flow = OAuthFlow(baseUrl: _base, clientId: _clientId, redirectUri: 'bpapp://callback');
final _storage = const FlutterSecureStorage();
/// Opens the consent screen and returns the stored tokens, or throws if the person declines.
Future<OAuthTokens> connect() async {
final pkce = Pkce.generate();
final state = base64UrlEncode(List<int>.generate(16, (_) => Random.secure().nextInt(256)));
final url = _flow.authorizeUrl(scopes: _scopes, state: state, pkce: pkce, purpose: 'self-care', lang: 'en');
final result = Uri.parse(await FlutterWebAuth2.authenticate(url: url.toString(), callbackUrlScheme: 'bpapp'));
if (result.queryParameters['state'] != state) throw StateError('state mismatch');
final code = result.queryParameters['code'];
if (code == null) throw StateError(result.queryParameters['error'] ?? 'no code');
final tokens = await _flow.exchange(code: code, pkce: pkce);
await _save(tokens);
return tokens;
}
/// The client for the connected record, refreshing and re-saving tokens as needed.
Future<(Anpheros, String)?> open() async {
final saved = await _storage.read(key: 'anpheros_tokens');
if (saved == null) return null;
final tokens = OAuthTokens.fromJson(jsonDecode(saved) as Map<String, dynamic>);
final client = Anpheros(
baseUrl: _base,
auth: AnpherosAuth.oauth(
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.expiresAt,
onRefresh: _flow.refresh,
onTokens: _save,
),
);
return (client, tokens.patient!);
}
Future<void> disconnect() async {
final saved = await _storage.read(key: 'anpheros_tokens');
if (saved != null) {
final tokens = OAuthTokens.fromJson(jsonDecode(saved) as Map<String, dynamic>);
await _flow.revoke(tokens.refreshToken ?? tokens.accessToken);
}
await _storage.delete(key: 'anpheros_tokens');
}
Future<void> _save(OAuthTokens tokens) =>
_storage.write(key: 'anpheros_tokens', value: jsonEncode(tokens.toJson()));
}
Auf dem Zustimmungsbildschirm meldet sich die Person an, sieht den Namen Ihrer App, den Zweck und genau das, wonach sie fragt, wählt den Datensatz aus (ihren eigenen, einen, der ihr wichtig ist, oder einen neuen für Ihre App) und legt fest, wie lange der Zugriff dauern soll. tokens.patient ist die ID dieses Datensatzes für Ihre App.
Refresh-Tokens rotieren: Jede Aktualisierung gibt ein neues Token zurück, und onTokens speichert es sofort. Die Wiederverwendung eines alten Refresh-Tokens widerruft die Berechtigung, überspringen Sie daher onTokens nicht.
Schritt 2: Messwerte lesen
// lib/readings.dart
import 'package:anpheros_sdk/anpheros_sdk.dart';
class Reading {
Reading(this.measuredAt, this.systolic, this.diastolic);
final DateTime measuredAt;
final double systolic;
final double diastolic;
}
const _bloodPressure = '85354-9', _systolic = '8480-6', _diastolic = '8462-4';
Future<List<Reading>> loadReadings(Anpheros anpheros, String patientId) async {
final page = await anpheros.observations.list(patientId, code: _bloodPressure, limit: 50);
return [
for (final Observation o in page.data)
if (o.effectiveDateTime != null)
Reading(
o.effectiveDateTime!,
o.components.firstWhere((c) => c.code == _systolic).value,
o.components.firstWhere((c) => c.code == _diastolic).value,
),
];
}
Future<void> addReading(Anpheros anpheros, String patientId, double systolic, double diastolic) =>
anpheros.observations.createObservation(
patientId,
ObservationInput(
code: _bloodPressure,
category: 'vital-signs',
effectiveAt: DateTime.now(),
components: [
Component(code: _systolic, value: systolic, unit: 'mm[Hg]'),
Component(code: _diastolic, value: diastolic, unit: 'mm[Hg]'),
],
authorType: AuthorType.patient,
),
sourceSystem: 'bp-app',
);
Messwerte werden als Standard-FHIR-Observations gespeichert, LOINC 85354-9 mit systolischen und diastolischen Komponenten, sodass der Arzt der Person, ein Kliniksystem oder eine andere App, mit der sie sich verbindet, diese ohne Kenntnis Ihrer App lesen kann. authorType: patient erfasst, dass die Person den Messwert eingegeben hat. Das SDK sendet bei jeder Erstellung einen Idempotenzschlüssel mit, sodass ein erneuter Versuch bei einer schlechten Verbindung den Messwert nicht zweimal hinzufügt.
Schritt 3: Der Bildschirm
// lib/main.dart
import 'package:anpheros_sdk/anpheros_sdk.dart';
import 'package:flutter/material.dart';
import 'readings.dart';
import 'record_connection.dart';
void main() => runApp(const MaterialApp(home: ReadingsScreen()));
class ReadingsScreen extends StatefulWidget {
const ReadingsScreen({super.key});
@override
State<ReadingsScreen> createState() => _ReadingsScreenState();
}
class _ReadingsScreenState extends State<ReadingsScreen> {
final _connection = RecordConnection();
Anpheros? _client;
String? _patient;
List<Reading> _readings = [];
String? _error;
@override
void initState() {
super.initState();
_open();
}
Future<void> _open() async {
final opened = await _connection.open();
if (opened == null) return;
setState(() {
_client = opened.$1;
_patient = opened.$2;
});
await _refresh();
}
Future<void> _refresh() async {
try {
final readings = await loadReadings(_client!, _patient!);
setState(() {
_readings = readings;
_error = null;
});
} on AnpherosException catch (e) {
if (e.status == 401) {
await _connection.disconnect(); // access was revoked or has expired
setState(() {
_client = null;
_patient = null;
_readings = [];
});
} else {
setState(() => _error = e.message);
}
}
}
@override
Widget build(BuildContext context) {
if (_client == null) {
return Scaffold(
body: Center(
child: FilledButton(
onPressed: () async {
await _connection.connect();
await _open();
},
child: const Text('Connect my health record'),
),
),
);
}
return Scaffold(
appBar: AppBar(title: const Text('Blood pressure')),
body: RefreshIndicator(
onRefresh: _refresh,
child: ListView(children: [
if (_error != null) ListTile(title: Text(_error!)),
for (final r in _readings)
ListTile(
title: Text('${r.systolic.round()}/${r.diastolic.round()} mmHg'),
subtitle: Text(r.measuredAt.toLocal().toString()),
),
]),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
await addReading(_client!, _patient!, 128, 82); // replace with a form
await _refresh();
},
child: const Icon(Icons.add),
),
);
}
}
Führen Sie es mit flutter run aus, tippen Sie auf Meine Gesundheitsakte verbinden, melden Sie sich auf dem Zustimmungsbildschirm an, erlauben Sie den Zugriff und fügen Sie dann einen Messwert hinzu.
Was die Scopes bewirken
Die App hat patient/Observation.rs?category=vital-signs und patient/Observation.c angefordert. Das bedeutet, sie kann Vitalparameter lesen und Beobachtungen hinzufügen, und nichts weiter. Eine Anfrage nach den Laborergebnissen, Diagnosen oder Dokumenten der Person beantwortet 403, und jeder andere Patient existiert für dieses Token nicht (404). Wenn die Person den Zugriff in Anpheros widerruft, beantwortet der nächste Aufruf 401, und der obige Bildschirm kehrt zu Connect zurück.
Fordern Sie nur dann mehr an, wenn eine Funktion dies erfordert, z. B. patient/MedicationStatement.rs für eine Medikamentenliste, und begründen Sie dies in Ihrer App, bevor Sie den Zustimmungsbildschirm öffnen.
Weiterführende Informationen
- Ein eigener Backend-Server. Wenn Ihr Produkt auch Serverfunktionen (Erinnerungen, Kliniker-Dashboards) bietet, lesen Sie How to build a healthcare API with TypeScript. Bewahren Sie API-Schlüssel auf dem Server und OAuth-Token auf dem Gerät auf.
- Diagramme und KI-Zusammenfassungen.
anpheros.context.buildgibt einen kontextbezogenen, quellengekennzeichneten Input für ein Modell zurück: How to build an AI healthcare app with a FHIR backend. - Rohes FHIR.
anpheros.fhirermöglicht Lese-, Such-, Erstellungs-, Transaktions-,$everything- und$summary-Operationen auf demselben Datensatz (How to store medical records using FHIR R4).
Häufig gestellte Fragen
Gibt es ein FHIR SDK für Flutter?
Ja. anpheros_sdk auf pub.dev ist ein Dart-Client für die Anpheros FHIR R4 und REST APIs, mit OAuth 2.1 / SMART on FHIR, PKCE, Token-Aktualisierung, idempotenten Schreibvorgängen und typisierten Modellen. Es funktioniert in Flutter unter iOS, Android, macOS und im Web.
Wo sollte eine Flutter-Gesundheits-App Tokens speichern?
Im sicheren Speicher der Plattform (Keychain unter iOS, Keystore-gestützter Speicher unter Android) über ein Paket wie flutter_secure_storage, niemals in Shared Preferences oder einfachen Dateien.
Benötigt die App ein eigenes Backend?
Nicht für die medizinischen Daten: Die App liest und schreibt den Datensatz der Person direkt mit deren Zustimmung. Sie benötigen ein Backend für alles andere, was Ihr Produkt tut, und um API-Schlüssel zu speichern, wenn Sie Datensätze im Namen von Personen erstellen.
Kann ich ohne echte Patienten testen?
Ja. Während der privaten Beta-Phase verwenden OAuth-Verbindungen Sandbox-Datensätze, und die Sandbox verfügt über 30 synthetische Patienten mit sechs Monaten Historie.
Verwandte Themen
- Patienteneinwilligung mit SMART on FHIR implementieren
- Eine Gesundheits-App erstellen: dieselben Bausteine, von der API-Seite
- Authentifizierung und OAuth
- Medical Data API
- Kostenlose FHIR-Datenbank und Sandbox
Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card