# Wie man eine Gesundheits-App mit Flutter erstellt

> Tutorial: Eine Flutter-App, die sich mit dem FHIR R4-Datensatz einer Person über OAuth-Einwilligung und PKCE verbindet, Tokens sicher speichert, Blutdruckwerte mit dem Dart SDK liest und hinzufügt und widerrufenen Zugriff behandelt.

Source: https://developers.anpheros.com/de/guides/healthcare-app-flutter

**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](https://developers.anpheros.com/de/guides/smart-on-fhir-backend) 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](https://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 deren `client_id`.

Während der privaten Beta verwenden OAuth-Verbindungen Sandbox-Datensätze, sodass Sie den gesamten Ablauf ohne reale Daten testen können.

```bash
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:

```xml
<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.

```dart
// 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

```dart
// 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

```dart
// 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](https://developers.anpheros.com/de/guides/healthcare-api-typescript). Bewahren Sie API-Schlüssel auf dem Server und OAuth-Token auf dem Gerät auf.
- **Diagramme und KI-Zusammenfassungen.** `anpheros.context.build` gibt einen kontextbezogenen, quellengekennzeichneten Input für ein Modell zurück: [How to build an AI healthcare app with a FHIR backend](https://developers.anpheros.com/de/guides/build-ai-healthcare-app).
- **Rohes FHIR.** `anpheros.fhir` ermöglicht Lese-, Such-, Erstellungs-, Transaktions-, `$everything`- und `$summary`-Operationen auf demselben Datensatz ([How to store medical records using FHIR R4](https://developers.anpheros.com/de/guides/store-medical-records-fhir)).

## 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](https://developers.anpheros.com/de/guides/smart-on-fhir-backend)
- [Eine Gesundheits-App erstellen](https://developers.anpheros.com/de/guides/build-a-healthcare-app): dieselben Bausteine, von der API-Seite
- [Authentifizierung und OAuth](https://developers.anpheros.com/de/guides/authentication)
- [Medical Data API](https://developers.anpheros.com/de/guides/medical-data-api)
- [Kostenlose FHIR-Datenbank und Sandbox](https://developers.anpheros.com/de/guides/free-fhir-database)
