Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card
Πώς να δημιουργήσετε μια εφαρμογή υγειονομικής περίθαλψης με Flutter
Οδηγός: μια εφαρμογή Flutter που συνδέεται στο αρχείο FHIR R4 ενός ατόμου με συναίνεση OAuth και PKCE, αποθηκεύει tokens με ασφάλεια, διαβάζει και προσθέτει μετρήσεις πίεσης αίματος με το Dart SDK και χειρίζεται την ανάκληση πρόσβασης.
Αυτός ο οδηγός δημιουργεί μια εφαρμογή Flutter που συνδέεται με το ιατρικό αρχείο HL7 FHIR R4 ενός ατόμου με τη συναίνεσή του, εμφανίζει τις μετρήσεις αρτηριακής του πίεσης και προσθέτει νέες, χρησιμοποιώντας το Dart SDK anpheros_sdk. Η εφαρμογή επικοινωνεί απευθείας με το Anpheros με ένα διακριτικό OAuth που έχει παραχωρήσει το άτομο, όχι με ένα κλειδί API, οπότε δεν χρειάζεται δικό της backend για τα ιατρικά δεδομένα. Εκτελείται σε iOS και Android. ο ίδιος κώδικας λειτουργεί σε macOS και στο web με τη ρύθμιση πλατφόρμας του πακέτου auth.
Πώς συνδέονται τα κομμάτια
┌──────────────────────┐ 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 │
└──────────────────────┘ ─────────────────────────────────────► └───────────────────────────┘
Μια εφαρμογή για κινητά δεν πρέπει ποτέ να περιέχει ένα κλειδί API: οποιοσδήποτε μπορεί να το εξαγάγει, και ένα κλειδί φτάνει σε κάθε ασθενή του έργου σας. Με το OAuth, το token στην εφαρμογή φτάνει σε μία εγγραφή, μόνο ό,τι ενέκρινε το άτομο, μέχρι να το ανακαλέσει. Η ροή είναι η τυπική αυτόνομη εκκίνηση SMART on FHIR που περιγράφεται στο Πώς να υλοποιήσετε τη συγκατάθεση ασθενούς με SMART on FHIR. Το SDK αναλαμβάνει την εργασία του πρωτοκόλλου.
Πριν ξεκινήσετε
- Flutter 3.22 ή νεότερη έκδοση.
- Ένα project Anpheros. Συνδεθείτε στο platform.anpheros.com/dashboard με τον λογαριασμό σας Google και λάβετε ένα κλειδί sandbox. Το project έρχεται με αυτό.
- Στο dashboard, καταχωρήστε μια εφαρμογή τύπου public με το redirect URI
bpapp://callback. Σημειώστε τοclient_id.
Κατά τη διάρκεια της ιδιωτικής beta, οι συνδέσεις OAuth χρησιμοποιούν sandbox records, οπότε μπορείτε να δοκιμάσετε ολόκληρη τη ροή χωρίς πραγματικά δεδομένα.
flutter create bp_app && cd bp_app
flutter pub add anpheros_sdk flutter_web_auth_2 flutter_secure_storage
Android: προσθέστε την callback activity του flutter_web_auth_2 μέσα στο <application> στο android/app/src/main/AndroidManifest.xml, με το δικό σας scheme:
<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 δεν χρειάζεται επιπλέον ρύθμιση για custom scheme.
Βήμα 1: Σύνδεση με τον φάκελο
Το OAuthFlow δημιουργεί τη διεύθυνση εξουσιοδότησης με μια πρόκληση PKCE. Το flutter_web_auth_2 την ανοίγει σε μια ασφαλή καρτέλα προγράμματος περιήγησης και επιστρέφει όταν το Anpheros ανακατευθύνει στο 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()));
}
Στην οθόνη συναίνεσης, το άτομο συνδέεται, βλέπει το όνομα της εφαρμογής σας, τον σκοπό και ακριβώς τι ζητά, επιλέγει τον φάκελο (τον δικό του, έναν που τον αφορά, ή έναν νέο για την εφαρμογή σας) και για πόσο καιρό διαρκεί η πρόσβαση. Το tokens.patient είναι το id αυτού του φακέλου για την εφαρμογή σας.
Τα refresh tokens περιστρέφονται: κάθε ανανέωση επιστρέφει ένα νέο, και το onTokens το αποθηκεύει αμέσως. Η επαναχρησιμοποίηση ενός παλιού refresh token ανακαλεί την παραχώρηση, οπότε μην παραλείψετε το onTokens.
Βήμα 2: Ανάγνωση των μετρήσεων
// 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',
);
Οι μετρήσεις αποθηκεύονται ως τυπικές FHIR Observations, LOINC 85354-9 με συστατικά συστολικής και διαστολικής πίεσης, έτσι ώστε ο γιατρός του ατόμου, ένα σύστημα κλινικής ή μια άλλη εφαρμογή που συνδέεται να τις διαβάζει χωρίς καμία γνώση της εφαρμογής σας. authorType: patient καταγράφει ότι το άτομο εισήγαγε τη μέτρηση. Το SDK στέλνει ένα κλειδί ιδιοδυναμίας (idempotency key) με κάθε δημιουργία, έτσι ώστε μια επανάληψη σε κακή σύνδεση να μην προσθέτει τη μέτρηση δύο φορές.
Βήμα 3: Η οθόνη
// 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),
),
);
}
}
Εκτελέστε το με flutter run, πατήστε Connect my health record, συνδεθείτε στην οθόνη συναίνεσης, επιτρέψτε την πρόσβαση και, στη συνέχεια, προσθέστε μια ανάγνωση.
Τι κάνουν τα scopes
Η εφαρμογή ζήτησε patient/Observation.rs?category=vital-signs και patient/Observation.c. Αυτό σημαίνει ότι μπορεί να διαβάσει ζωτικά σημεία και να προσθέσει παρατηρήσεις, και τίποτα άλλο. Ένα αίτημα για εργαστηριακά αποτελέσματα, διαγνώσεις ή έγγραφα του ατόμου απαντά 403, και κανένας άλλος ασθενής δεν υπάρχει για αυτό το token (404). Εάν το άτομο ανακαλέσει την πρόσβαση στο Anpheros, η επόμενη κλήση απαντά 401, και η παραπάνω οθόνη επιστρέφει στο Connect.
Ζητήστε περισσότερα μόνο όταν μια λειτουργία το χρειάζεται, για παράδειγμα patient/MedicationStatement.rs για μια λίστα φαρμάκων, και εξηγήστε γιατί στην εφαρμογή σας πριν ανοίξετε την οθόνη συναίνεσης.
Περαιτέρω Βήματα
- Δικό σας backend. Εάν το προϊόν σας διαθέτει επίσης λειτουργίες διακομιστή (υπενθυμίσεις, πίνακες ελέγχου κλινικών), ανατρέξτε στο Πώς να δημιουργήσετε ένα API υγειονομικής περίθαλψης με TypeScript. Διατηρήστε τα κλειδιά API στον διακομιστή και τα διακριτικά OAuth στη συσκευή.
- Γραφήματα και περιλήψεις AI. Το
anpheros.context.buildεπιστρέφει ένα πλαίσιο με ετικέτες πηγής για ένα μοντέλο: Πώς να δημιουργήσετε μια εφαρμογή υγειονομικής περίθαλψης AI με backend FHIR. - Ακατέργαστο FHIR. Το
anpheros.fhirπαρέχει ανάγνωση, αναζήτηση, δημιουργία, συναλλαγή,$everythingκαι$summaryστο ίδιο αρχείο καταγραφής (Πώς να αποθηκεύσετε ιατρικά αρχεία χρησιμοποιώντας FHIR R4).
Συχνές Ερωτήσεις
Υπάρχει SDK FHIR για Flutter;
Ναι. Το anpheros_sdk στο pub.dev είναι ένας Dart client για τα Anpheros FHIR R4 και REST APIs, με OAuth 2.1 / SMART on FHIR, PKCE, ανανέωση token, idempotent writes και typed models. Λειτουργεί σε Flutter σε iOS, Android, macOS και web.
Πού πρέπει μια εφαρμογή υγείας Flutter να αποθηκεύει τα tokens;
Στον ασφαλή αποθηκευτικό χώρο της πλατφόρμας (Keychain σε iOS, Keystore-backed storage σε Android) μέσω ενός πακέτου όπως το flutter_secure_storage, ποτέ σε shared preferences ή απλά αρχεία.
Χρειάζεται η εφαρμογή δικό της backend;
Όχι για τα ιατρικά δεδομένα: η εφαρμογή διαβάζει και γράφει το αρχείο του ατόμου απευθείας με τη συγκατάθεσή του. Χρειάζεστε ένα backend για οτιδήποτε άλλο κάνει το προϊόν σας, και για να κρατάτε API keys εάν δημιουργείτε αρχεία για λογαριασμό ατόμων.
Μπορώ να κάνω δοκιμές χωρίς πραγματικούς ασθενείς;
Ναι. Κατά τη διάρκεια της ιδιωτικής beta, οι συνδέσεις OAuth χρησιμοποιούν sandbox αρχεία, και το sandbox έχει 30 συνθετικούς ασθενείς με έξι μήνες ιστορικό.
Σχετικά
- Πώς να υλοποιήσετε τη συγκατάθεση ασθενούς με SMART on FHIR
- Δημιουργία εφαρμογής υγειονομικής περίθαλψης: τα ίδια δομικά στοιχεία, από την πλευρά του API
- Έλεγχος ταυτότητας και OAuth
- Ιατρικό API δεδομένων
- Δωρεάν βάση δεδομένων FHIR και sandbox
Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card