# Cum să construiești o aplicație de sănătate cu Flutter

> Tutorial: o aplicație Flutter care se conectează la înregistrarea FHIR R4 a unei persoane cu consimțământ OAuth și PKCE, stochează token-uri în siguranță, citește și adaugă citiri ale tensiunii arteriale cu SDK-ul Dart, și gestionează accesul revocat.

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

**Acest tutorial construiește o aplicație Flutter care se conectează la înregistrarea medicală HL7 FHIR R4 a unei persoane cu consimțământul acesteia, afișează citirile tensiunii arteriale și adaugă altele noi, folosind SDK-ul Dart `anpheros_sdk`.** Aplicația comunică direct cu Anpheros folosind un token OAuth acordat de persoană, nu o cheie API, deci nu necesită un backend propriu pentru datele medicale. Rulează pe iOS și Android; același cod funcționează pe macOS și web cu configurarea platformei pachetului de autentificare.

## Cum se potrivesc piesele

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

O aplicație mobilă nu trebuie să conțină niciodată o cheie API: oricine o poate extrage, iar o cheie ajunge la toți pacienții proiectului dumneavoastră. Cu OAuth, tokenul din aplicație ajunge la un singur înregistrare, doar ceea ce persoana a aprobat, până când îl revocă. Fluxul este lansarea standard SMART on FHIR standalone descrisă în [Cum se implementează consimțământul pacientului cu SMART on FHIR](https://developers.anpheros.com/ro/guides/smart-on-fhir-backend); SDK-ul se ocupă de protocol.

## Înainte de a începe

- Flutter 3.22 sau o versiune mai nouă.
- Un proiect Anpheros. Autentificați-vă la [platform.anpheros.com/dashboard](https://platform.anpheros.com/dashboard/) cu Google și obțineți o cheie sandbox; proiectul vine cu ea.
- În dashboard, înregistrați o aplicație de tip **public** cu URI-ul de redirecționare `bpapp://callback`. Notați `client_id`.

În timpul beta-ului privat, conexiunile OAuth folosesc înregistrări sandbox, astfel încât puteți testa întregul flux fără date reale.

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

**Android:** adăugați activitatea de callback a `flutter_web_auth_2` în interiorul `<application>` din `android/app/src/main/AndroidManifest.xml`, cu schema dvs.:

```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 nu necesită configurări suplimentare pentru o schemă personalizată.

## Pasul 1: Conectarea la înregistrare

`OAuthFlow` construiește adresa de autorizare cu un PKCE challenge; `flutter_web_auth_2` o deschide într-o filă de browser securizată și revine când Anpheros redirecționează către `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()));
}
```

Pe ecranul de consimțământ, persoana se autentifică, vede numele aplicației dvs., scopul și exact ce solicită, alege înregistrarea (propria sa, una de care îi pasă sau una nouă pentru aplicația dvs.) și cât timp durează accesul. `tokens.patient` este ID-ul acelei înregistrări pentru aplicația dvs.

Token-urile de reîmprospătare se rotesc: fiecare reîmprospătare returnează unul nou, iar `onTokens` îl stochează imediat. Reutilizarea unui token de reîmprospătare vechi revocă permisiunea, așa că nu omiteți `onTokens`.

## Pasul 2: Citirea valorilor

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

Valorile sunt stocate ca Observații FHIR standard, LOINC `85354-9` cu componente sistolice și diastolice, astfel încât medicul persoanei, un sistem de clinică sau o altă aplicație la care se conectează să le citească fără nicio cunoaștere a aplicației dumneavoastră. `authorType: patient` înregistrează că persoana a introdus valoarea. SDK-ul trimite o cheie de idempotenta cu fiecare creare, astfel încât o reîncercare pe o conexiune proastă nu adaugă valoarea de două ori.

## Pasul 3: ecranul

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

Rulați-l cu `flutter run`, apăsați **Connect my health record**, autentificați-vă pe ecranul de consimțământ, permiteți accesul, apoi adăugați o înregistrare.

## Ce fac scopurile

Aplicația a solicitat `patient/Observation.rs?category=vital-signs` și `patient/Observation.c`. Aceasta înseamnă că poate citi semne vitale și adăuga observații, și nimic altceva. O cerere pentru rezultatele de laborator ale persoanei, diagnostice sau documente răspunde `403`, iar orice alt pacient nu există pentru acest token (`404`). Dacă persoana revocă accesul în Anpheros, următorul apel răspunde `401`, iar ecranul de mai sus revine la **Connect**.

Solicitați mai mult doar atunci când o funcționalitate o necesită, de exemplu `patient/MedicationStatement.rs` pentru o listă de medicamente, și explicați motivul în aplicația dvs. înainte de a deschide ecranul de consimțământ.

## Mergi mai departe

- **Un backend propriu.** Dacă produsul tău are și funcționalități server (memento-uri, tablouri de bord pentru clinicieni), vezi [Cum să construiești un API de sănătate cu TypeScript](https://developers.anpheros.com/ro/guides/healthcare-api-typescript). Păstrează cheile API pe server și token-urile OAuth pe dispozitiv.
- **Diagrame și rezumate AI.** `anpheros.context.build` returnează un context etichetat sursă pentru un model: [Cum să construiești o aplicație AI de sănătate cu un backend FHIR](https://developers.anpheros.com/ro/guides/build-ai-healthcare-app).
- **FHIR brut.** `anpheros.fhir` oferă operațiuni de citire, căutare, creare, tranzacție, `$everything` și `$summary` pe același înregistrare ([Cum să stochezi înregistrări medicale folosind FHIR R4](https://developers.anpheros.com/ro/guides/store-medical-records-fhir)).

## Întrebări frecvente

### Există un SDK FHIR pentru Flutter?
Da. `anpheros_sdk` pe pub.dev este un client Dart pentru API-urile Anpheros FHIR R4 și REST, cu OAuth 2.1 / SMART on FHIR, PKCE, reîmprospătare token, scrieri idempotente și modele tipizate. Funcționează în Flutter pe iOS, Android, macOS și web.

### Unde ar trebui o aplicație de sănătate Flutter să stocheze token-urile?
În stocarea securizată a platformei (Keychain pe iOS, stocare bazată pe Keystore pe Android) printr-un pachet precum `flutter_secure_storage`, niciodată în preferințe partajate sau fișiere simple.

### Aplicația are nevoie de propriul backend?
Nu pentru datele medicale: aplicația citește și scrie direct în registrul persoanei cu consimțământul acesteia. Aveți nevoie de un backend pentru orice altceva face produsul dvs. și pentru a păstra cheile API dacă creați registre în numele persoanelor.

### Pot testa fără pacienți reali?
Da. În timpul beta-ului privat, conexiunile OAuth utilizează registre sandbox, iar sandbox-ul are 30 de pacienți sintetici cu șase luni de istoric.

## Conexe

- [Cum să implementați consimțământul pacientului cu SMART on FHIR](https://developers.anpheros.com/ro/guides/smart-on-fhir-backend)
- [Construiți o aplicație de sănătate](https://developers.anpheros.com/ro/guides/build-a-healthcare-app): aceleași blocuri de construcție, din partea API-ului
- [Autentificare și OAuth](https://developers.anpheros.com/ro/guides/authentication)
- [API de date medicale](https://developers.anpheros.com/ro/guides/medical-data-api)
- [Bază de date FHIR gratuită și sandbox](https://developers.anpheros.com/ro/guides/free-fhir-database)
