# Comment créer une application de santé avec Flutter

> Tutoriel : une application Flutter qui se connecte au dossier FHIR R4 d'une personne avec consentement OAuth et PKCE, stocke les jetons en toute sécurité, lit et ajoute des mesures de tension artérielle avec le SDK Dart, et gère les accès révoqués.

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

**Ce tutoriel crée une application Flutter qui se connecte au dossier médical HL7 FHIR R4 d'une personne avec son consentement, affiche ses mesures de tension artérielle et en ajoute de nouvelles, en utilisant le SDK Dart `anpheros_sdk`.** L'application communique directement avec Anpheros avec un jeton OAuth que la personne a accordé, et non avec une clé API, elle n'a donc pas besoin de son propre backend pour les données médicales. Elle s'exécute sur iOS et Android ; le même code fonctionne sur macOS et le web avec la configuration de plateforme du package d'authentification.

## Comment les pièces s'assemblent

```
┌──────────────────────┐ 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     │
└──────────────────────┘ ─────────────────────────────────────► └───────────────────────────┘
```

Une application mobile ne doit jamais contenir de clé d'API : n'importe qui peut l'extraire, et une clé atteint tous les patients de votre projet. Avec OAuth, le jeton dans l'application atteint un enregistrement, uniquement ce que la personne a approuvé, jusqu'à ce qu'elle le révoque. Le flux est le lancement autonome standard SMART on FHIR décrit dans [Comment implémenter le consentement du patient avec SMART on FHIR](https://developers.anpheros.com/fr/guides/smart-on-fhir-backend) ; le SDK s'occupe du travail de protocole.

## Avant de commencer

- Flutter 3.22 ou plus récent.
- Un projet Anpheros. Connectez-vous sur [platform.anpheros.com/dashboard](https://platform.anpheros.com/dashboard/) avec Google et obtenez une clé sandbox ; le projet est fourni avec.
- Dans le tableau de bord, enregistrez une application de type **public** avec l'URI de redirection `bpapp://callback`. Notez son `client_id`.

Pendant la bêta privée, les connexions OAuth utilisent des enregistrements sandbox, vous pouvez donc tester l'intégralité du flux sans données réelles.

```bash
flutter create bp_app && cd bp_app
flutter pub add anpheros_sdk flutter_web_auth_2 flutter_secure_storage
```

**Android :** ajoutez l'activité de rappel de `flutter_web_auth_2` à l'intérieur de `<application>` dans `android/app/src/main/AndroidManifest.xml`, avec votre schéma :

```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 ne nécessite aucune configuration supplémentaire pour un schéma personnalisé.

## Étape 1 : se connecter au dossier

`OAuthFlow` construit l'adresse d'autorisation avec un défi PKCE ; `flutter_web_auth_2` l'ouvre dans un onglet de navigateur sécurisé et revient lorsque Anpheros redirige vers `bpapp://callback`.

```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()));
}
```

Sur l'écran de consentement, la personne se connecte, voit le nom de votre application, son objectif et exactement ce qu'elle demande, choisit le dossier (le sien, celui dont elle s'occupe, ou un nouveau pour votre application) et la durée de l'accès. `tokens.patient` est l'identifiant de ce dossier pour votre application.

Les jetons de rafraîchissement tournent : chaque rafraîchissement renvoie un nouveau jeton, et `onTokens` le stocke immédiatement. La réutilisation d'un ancien jeton de rafraîchissement révoque l'autorisation, alors ne sautez pas `onTokens`.

## Étape 2 : lire les relevés

```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',
    );
```

Les relevés sont stockés sous forme d'Observations FHIR standard, LOINC `85354-9` avec des composantes systolique et diastolique, de sorte que le médecin de la personne, un système de clinique ou une autre application à laquelle elle se connecte les lit sans aucune connaissance de votre application. `authorType: patient` enregistre que la personne a saisi le relevé. Le SDK envoie une clé d'idempotence avec chaque création, de sorte qu'une nouvelle tentative en cas de mauvaise connexion n'ajoute pas le relevé deux fois.

## Étape 3 : l'écran

```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),
      ),
    );
  }
}
```

Exécutez-le avec `flutter run`, appuyez sur **Connect my health record**, connectez-vous sur l'écran de consentement, autorisez l'accès, puis ajoutez une lecture.

## Ce que font les scopes

L'application a demandé `patient/Observation.rs?category=vital-signs` et `patient/Observation.c`. Cela signifie qu'elle peut lire les signes vitaux et ajouter des observations, et rien d'autre. Une requête pour les résultats de laboratoire, les diagnostics ou les documents de la personne répond à `403`, et tout autre patient n'existe pas pour ce token (`404`). Si la personne révoque l'accès dans Anpheros, le prochain appel répondra `401`, et l'écran ci-dessus reviendra à **Connect**.

Demandez-en plus uniquement lorsqu'une fonctionnalité en a besoin, par exemple `patient/MedicationStatement.rs` pour une liste de médicaments, et expliquez pourquoi dans votre application avant d'ouvrir l'écran de consentement.

## Aller plus loin

- **Votre propre backend.** Si votre produit possède également des fonctionnalités serveur (rappels, tableaux de bord cliniques), consultez [Comment construire une API de santé avec TypeScript](https://developers.anpheros.com/fr/guides/healthcare-api-typescript). Conservez les clés API sur le serveur et les jetons OAuth sur l'appareil.
- **Graphiques et résumés IA.** `anpheros.context.build` renvoie un contexte étiqueté par source pour un modèle : [Comment construire une application de santé IA avec un backend FHIR](https://developers.anpheros.com/fr/guides/build-ai-healthcare-app).
- **FHIR brut.** `anpheros.fhir` permet la lecture, la recherche, la création, les transactions, `$everything` et `$summary` sur le même enregistrement ([Comment stocker des dossiers médicaux en utilisant FHIR R4](https://developers.anpheros.com/fr/guides/store-medical-records-fhir)).

## Questions fréquemment posées

### Existe-t-il un SDK FHIR pour Flutter ?
Oui. `anpheros_sdk` sur pub.dev est un client Dart pour les API Anpheros FHIR R4 et REST, avec OAuth 2.1 / SMART on FHIR, PKCE, rafraîchissement de jeton, écritures idempotentes et modèles typés. Il fonctionne dans Flutter sur iOS, Android, macOS et le web.

### Où une application de santé Flutter doit-elle stocker les jetons ?
Dans le stockage sécurisé de la plateforme (Keychain sur iOS, stockage basé sur Keystore sur Android) via un package tel que `flutter_secure_storage`, jamais dans les préférences partagées ou les fichiers en clair.

### L'application a-t-elle besoin de son propre backend ?
Pas pour les données médicales : l'application lit et écrit directement le dossier de la personne avec son consentement. Vous avez besoin d'un backend pour tout ce que votre produit fait d'autre, et pour conserver les clés API si vous créez des dossiers au nom de personnes.

### Puis-je tester sans vrais patients ?
Oui. Pendant la bêta privée, les connexions OAuth utilisent des enregistrements sandbox, et la sandbox contient 30 patients synthétiques avec six mois d'historique.

## Liens Utiles

- [Comment implémenter le consentement du patient avec SMART on FHIR](https://developers.anpheros.com/fr/guides/smart-on-fhir-backend)
- [Créer une application de santé](https://developers.anpheros.com/fr/guides/build-a-healthcare-app) : les mêmes blocs de construction, du côté de l'API
- [Authentification et OAuth](https://developers.anpheros.com/fr/guides/authentication)
- [API de données médicales](https://developers.anpheros.com/fr/guides/medical-data-api)
- [Base de données FHIR gratuite et sandbox](https://developers.anpheros.com/fr/guides/free-fhir-database)
