AnpherosAnpheros PlatformDevelopers

Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card

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

Tutorial: o aplicație Flutter care se conectează la fișierul FHIR R4 al unei persoane cu consimțământ OAuth și PKCE, stochează token-uri în siguranță, citește și adaugă măsurători de tensiune arterială cu SDK-ul Dart și gestionează accesul revocat.

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; SDK-ul se ocupă de protocol.

Înainte de a începe

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

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.:

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

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

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

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

Î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

Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card

Product updates by email

New features, guides and the end of the beta. One email a month at most.

Send me occasional Anpheros Platform updates by email (new features, API changes, guides). I'll confirm by email and can unsubscribe in one click from any email. Privacy