# Cómo crear una aplicación de salud con Flutter

> Tutorial: una aplicación Flutter que se conecta al registro FHIR R4 de una persona con consentimiento OAuth y PKCE, almacena tokens de forma segura, lee y agrega lecturas de presión arterial con el SDK Dart, y maneja el acceso revocado.

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

**Este tutorial crea una aplicación Flutter que se conecta al registro médico HL7 FHIR R4 de una persona con su consentimiento, muestra sus lecturas de presión arterial y agrega nuevas, utilizando el SDK Dart `anpheros_sdk`.** La aplicación se comunica directamente con Anpheros con un token OAuth que la persona otorgó, no con una clave API, por lo que no necesita su propio backend para los datos médicos. Se ejecuta en iOS y Android; el mismo código funciona en macOS y la web con la configuración de plataforma del paquete de autenticación.

## Cómo encajan las piezas

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

Una aplicación móvil nunca debe contener una clave de API: cualquiera puede extraerla, y una clave llega a todos los pacientes de su proyecto. Con OAuth, el token en la aplicación llega a un registro, solo lo que la persona aprobó, hasta que lo revoque. El flujo es el lanzamiento independiente estándar de SMART on FHIR descrito en [Cómo implementar el consentimiento del paciente con SMART on FHIR](https://developers.anpheros.com/es/guides/smart-on-fhir-backend); el SDK se encarga del trabajo del protocolo.

## Antes de empezar

- Flutter 3.22 o superior.
- Un proyecto Anpheros. Inicia sesión en [platform.anpheros.com/dashboard](https://platform.anpheros.com/dashboard/) con Google y obtén una clave de sandbox; el proyecto viene con ella.
- En el dashboard, registra una aplicación de tipo **pública** con la URI de redirección `bpapp://callback`. Anota su `client_id`.

Durante la beta privada, las conexiones OAuth utilizan registros de sandbox, por lo que puedes probar todo el flujo sin datos reales.

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

**Android:** añade la actividad de callback de `flutter_web_auth_2` dentro de `<application>` en `android/app/src/main/AndroidManifest.xml`, con tu esquema:

```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 no necesita configuración adicional para un esquema personalizado.

## Paso 1: Conectar al registro

`OAuthFlow` construye la dirección de autorización con un desafío PKCE; `flutter_web_auth_2` la abre en una pestaña segura del navegador y regresa cuando Anpheros redirige 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()));
}
```

En la pantalla de consentimiento, la persona inicia sesión, ve el nombre de su aplicación, el propósito y exactamente lo que solicita, elige el registro (el suyo propio, uno que le interese o uno nuevo para su aplicación) y cuánto tiempo dura el acceso. `tokens.patient` es el id de ese registro para su aplicación.

Los tokens de actualización rotan: cada actualización devuelve uno nuevo, y `onTokens` lo almacena de inmediato. Reutilizar un token de actualización antiguo revoca la concesión, así que no omita `onTokens`.

## Paso 2: leer las lecturas

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

Las lecturas se almacenan como Observaciones FHIR estándar, LOINC `85354-9` con componentes sistólica y diastólica, de modo que el médico de la persona, un sistema de clínica u otra aplicación a la que se conecten las lean sin ningún conocimiento de su aplicación. `authorType: patient` registra que la persona introdujo la lectura. El SDK envía una clave de idempotencia con cada creación, por lo que un reintento en una conexión defectuosa no añade la lectura dos veces.

## Paso 3: la pantalla

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

Ejecútelo con `flutter run`, toque **Conectar mi registro de salud**, inicie sesión en la pantalla de consentimiento, permita el acceso y luego agregue una lectura.

## Qué hacen los ámbitos

La aplicación solicitó `patient/Observation.rs?category=vital-signs` y `patient/Observation.c`. Eso significa que puede leer signos vitales y agregar observaciones, y nada más. Una solicitud de resultados de laboratorio, diagnósticos o documentos de la persona responde `403`, y ningún otro paciente existe para este token (`404`). Si la persona revoca el acceso en Anpheros, la siguiente llamada responde `401`, y la pantalla anterior vuelve a **Conectar**.

Solicite más solo cuando una función lo necesite, por ejemplo `patient/MedicationStatement.rs` para una lista de medicamentos, y explique por qué en su aplicación antes de abrir la pantalla de consentimiento.

## Profundizando

- **Un backend propio.** Si tu producto también tiene funcionalidades de servidor (recordatorios, paneles para clínicos), consulta [Cómo crear una API de salud con TypeScript](https://developers.anpheros.com/es/guides/healthcare-api-typescript). Mantén las API keys en el servidor y los tokens OAuth en el dispositivo.
- **Gráficos y resúmenes de IA.** `anpheros.context.build` devuelve un contexto etiquetado por fuente para un modelo: [Cómo crear una aplicación de salud con IA y un backend FHIR](https://developers.anpheros.com/es/guides/build-ai-healthcare-app).
- **FHIR sin procesar.** `anpheros.fhir` proporciona operaciones de lectura, búsqueda, creación, transacción, `$everything` y `$summary` sobre el mismo registro ([Cómo almacenar registros médicos usando FHIR R4](https://developers.anpheros.com/es/guides/store-medical-records-fhir)).

## Preguntas frecuentes

### ¿Existe un SDK de FHIR para Flutter?
Sí. `anpheros_sdk` en pub.dev es un cliente Dart para las API REST y FHIR R4 de Anpheros, con OAuth 2.1 / SMART on FHIR, PKCE, actualización de tokens, escrituras idempotentes y modelos tipados. Funciona en Flutter para iOS, Android, macOS y la web.

### ¿Dónde debe una aplicación de salud Flutter almacenar los tokens?
En el almacenamiento seguro de la plataforma (Keychain en iOS, almacenamiento respaldado por Keystore en Android) a través de un paquete como `flutter_secure_storage`, nunca en preferencias compartidas o archivos planos.

### ¿La aplicación necesita su propio backend?
No para los datos médicos: la aplicación lee y escribe el registro de la persona directamente con su consentimiento. Necesitará un backend para cualquier otra cosa que haga su producto, y para almacenar claves de API si crea registros en nombre de las personas.

### ¿Puedo probar sin pacientes reales?
Sí. Durante la beta privada, las conexiones OAuth utilizan registros de sandbox, y el sandbox tiene 30 pacientes sintéticos con seis meses de historial.

## Relacionado

- [Cómo implementar el consentimiento del paciente con SMART on FHIR](https://developers.anpheros.com/es/guides/smart-on-fhir-backend)
- [Crea una aplicación de salud](https://developers.anpheros.com/es/guides/build-a-healthcare-app): los mismos bloques de construcción, desde el lado de la API
- [Autenticación y OAuth](https://developers.anpheros.com/es/guides/authentication)
- [API de datos médicos](https://developers.anpheros.com/es/guides/medical-data-api)
- [Base de datos FHIR y sandbox gratuitos](https://developers.anpheros.com/es/guides/free-fhir-database)
