# Come creare un'app sanitaria con Flutter

> Tutorial: un'app Flutter che si connette al record FHIR R4 di una persona con consenso OAuth e PKCE, memorizza i token in modo sicuro, legge e aggiunge letture della pressione sanguigna con l'SDK Dart, e gestisce l'accesso revocato.

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

**Questo tutorial crea un'app Flutter che si connette al record medico HL7 FHIR R4 di una persona con il suo consenso, mostra le sue letture della pressione sanguigna e ne aggiunge di nuove, utilizzando l'SDK Dart `anpheros_sdk`.** L'app comunica direttamente con Anpheros tramite un token OAuth concesso dalla persona, non con una chiave API, quindi non necessita di un proprio backend per i dati medici. Funziona su iOS e Android; lo stesso codice funziona su macOS e sul web con la configurazione della piattaforma del pacchetto di autenticazione.

## Come i pezzi si incastrano

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

Un'app mobile non deve mai contenere una chiave API: chiunque può estrarla e una chiave raggiunge ogni paziente del tuo progetto. Con OAuth, il token nell'app raggiunge un record, solo ciò che la persona ha approvato, finché non lo revoca. Il flusso è il lancio standalone standard SMART on FHIR descritto in [Come implementare il consenso del paziente con SMART on FHIR](https://developers.anpheros.com/it/guides/smart-on-fhir-backend); l'SDK si occupa del lavoro di protocollo.

## Prima di iniziare

- Flutter 3.22 o versione successiva.
- Un progetto Anpheros. Accedi a [platform.anpheros.com/dashboard](https://platform.anpheros.com/dashboard/) con Google e ottieni una sandbox key; il progetto è fornito con essa.
- Nella dashboard, registra un'applicazione di tipo **public** con il redirect URI `bpapp://callback`. Prendi nota del suo `client_id`.

Durante la beta privata, le connessioni OAuth utilizzano record sandbox, quindi puoi testare l'intero flusso senza dati reali.

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

**Android:** aggiungi l'activity di callback di `flutter_web_auth_2` all'interno di `<application>` in `android/app/src/main/AndroidManifest.xml`, con il tuo 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 non richiede configurazioni aggiuntive per uno schema personalizzato.

## Passaggio 1: connettersi al record

`OAuthFlow` costruisce l'indirizzo di autorizzazione con una sfida PKCE; `flutter_web_auth_2` lo apre in una scheda del browser sicura e restituisce quando Anpheros reindirizza a `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()));
}
```

Nella schermata del consenso, la persona accede, vede il nome della tua app, lo scopo e esattamente cosa richiede, sceglie il record (il proprio, uno a cui tiene o uno nuovo per la tua app) e per quanto tempo dura l'accesso. `tokens.patient` è l'id di quel record per la tua app.

I token di aggiornamento ruotano: ogni aggiornamento restituisce uno nuovo e `onTokens` lo memorizza subito. Riutilizzare un vecchio token di aggiornamento revoca la concessione, quindi non saltare `onTokens`.

## Passaggio 2: leggere le letture

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

Le letture sono memorizzate come osservazioni FHIR standard, LOINC `85354-9` con componenti sistolica e diastolica, in modo che il medico della persona, un sistema di clinica o un'altra app a cui si connettono le legga senza alcuna conoscenza della tua app. `authorType: patient` registra che la persona ha inserito la lettura. L'SDK invia una chiave di idempotenza con ogni creazione, quindi un nuovo tentativo in caso di connessione non riuscita non aggiunge la lettura due volte.

## Passaggio 3: lo schermo

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

Eseguilo con `flutter run`, tocca **Connetti la mia cartella clinica**, accedi alla schermata di consenso, consenti l'accesso, quindi aggiungi una lettura.

## Cosa fanno gli scope

L'app ha richiesto `patient/Observation.rs?category=vital-signs` e `patient/Observation.c`. Ciò significa che può leggere i segni vitali e aggiungere osservazioni, e nient'altro. Una richiesta per i risultati di laboratorio, le diagnosi o i documenti della persona risponde `403`, e qualsiasi altro paziente non esiste per questo token (`404`). Se la persona revoca l'accesso in Anpheros, la chiamata successiva risponde `401`, e la schermata sopra torna a **Connect**.

Richiedi di più solo quando una funzionalità lo necessita, ad esempio `patient/MedicationStatement.rs` per un elenco di farmaci, e spiega il motivo nella tua app prima di aprire la schermata di consenso.

## Approfondimenti

- **Un backend personalizzato.** Se il tuo prodotto dispone anche di funzionalità server (promemoria, dashboard per clinici), consulta [Come creare un'API sanitaria con TypeScript](https://developers.anpheros.com/it/guides/healthcare-api-typescript). Conserva le API key sul server e i token OAuth sul dispositivo.
- **Grafici e riepiloghi AI.** `anpheros.context.build` restituisce un contesto etichettato per sorgente per un modello: [Come creare un'app sanitaria AI con un backend FHIR](https://developers.anpheros.com/it/guides/build-ai-healthcare-app).
- **FHIR grezzo.** `anpheros.fhir` fornisce operazioni di lettura, ricerca, creazione, transazione, `$everything` e `$summary` sullo stesso record ([Come archiviare cartelle cliniche utilizzando FHIR R4](https://developers.anpheros.com/it/guides/store-medical-records-fhir)).

## Domande frequenti

### Esiste un SDK FHIR per Flutter?
Sì. `anpheros_sdk` su pub.dev è un client Dart per le API Anpheros FHIR R4 e REST, con OAuth 2.1 / SMART on FHIR, PKCE, token refresh, scritture idempotenti e modelli tipizzati. Funziona in Flutter su iOS, Android, macOS e web.

### Dove dovrebbe un'app sanitaria Flutter memorizzare i token?
Nello storage sicuro della piattaforma (Keychain su iOS, storage basato su Keystore su Android) tramite un pacchetto come `flutter_secure_storage`, mai nelle shared preferences o in file di testo semplice.

### L'app necessita di un proprio backend?
Non per i dati medici: l'app legge e scrive il record della persona direttamente con il suo consenso. È necessario un backend per qualsiasi altra cosa faccia il tuo prodotto e per conservare le API key se crei record per conto delle persone.

### Posso testare senza pazienti reali?
Sì. Durante la beta privata, le connessioni OAuth utilizzano record sandbox e la sandbox ha 30 pazienti sintetici con sei mesi di cronologia.

## Correlati

- [Come implementare il consenso del paziente con SMART on FHIR](https://developers.anpheros.com/it/guides/smart-on-fhir-backend)
- [Costruisci un'app sanitaria](https://developers.anpheros.com/it/guides/build-a-healthcare-app): gli stessi blocchi costruttivi, dal lato API
- [Autenticazione e OAuth](https://developers.anpheros.com/it/guides/authentication)
- [API dati medici](https://developers.anpheros.com/it/guides/medical-data-api)
- [Database FHIR gratuito e sandbox](https://developers.anpheros.com/it/guides/free-fhir-database)
