Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card
Як створити медичний додаток за допомогою Flutter
Посібник: додаток Flutter, який підключається до запису FHIR R4 особи за допомогою згоди OAuth та PKCE, безпечно зберігає токени, читає та додає показники артеріального тиску за допомогою Dart SDK та обробляє відкликану згоду.
Цей посібник створює додаток Flutter, який підключається до медичного запису HL7 FHIR R4 особи за її згодою, показує показники артеріального тиску та додає нові, використовуючи Dart SDK anpheros_sdk. Додаток спілкується з Anpheros безпосередньо за допомогою OAuth токена, наданого особою, а не за допомогою API ключа, тому йому не потрібен власний бекенд для медичних даних. Він працює на iOS та Android; той самий код працює на macOS та веб з налаштуванням платформи для пакету автентифікації.
Як все взаємопов'язано
┌──────────────────────┐ 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 токен у додатку надає доступ до одного запису, лише до того, що схвалила особа, доки вона його не відкличе. Потік відповідає стандартному запуску SMART on FHIR standalone, описаному в Як реалізувати згоду пацієнта за допомогою SMART on FHIR; SDK виконує роботу з протоколом.
Перед початком роботи
- Flutter 3.22 або новіша версія.
- Проєкт Anpheros. Увійдіть на platform.anpheros.com/dashboard за допомогою Google та отримайте ключ sandbox; проєкт постачається з ним.
- На панелі керування зареєструйте застосунок типу public з URI перенаправлення
bpapp://callback. Зверніть увагу на йогоclient_id.
Під час закритого бета-тестування OAuth-з'єднання використовують записи sandbox, тому ви можете протестувати весь процес без реальних даних.
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, з вашою схемою:
<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 не потребує додаткових налаштувань для користувацької схеми.
Крок 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 — це ідентифікатор цього запису для вашого додатка.
Токени оновлення обертаються: кожне оновлення повертає новий, і onTokens негайно зберігає його. Повторне використання старого токена оновлення відкликає дозвіл, тому не пропускайте 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, LOINC 85354-9 із компонентами систолічного та діастолічного тиску, тому лікар особи, клінічна система або інший додаток, до якого вони підключаються, читають їх без будь-яких знань про ваш додаток. authorType: patient записує, що особа ввела показник. SDK надсилає ключ ідемпотентності з кожним створенням, тому повторна спроба при поганому з'єднанні не додає показник двічі.
Крок 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, а будь-який інший пацієнт не існує для цього токена (404). Якщо особа відкликає доступ в Anpheros, наступний виклик відповідає 401, і екран вище повертається до Connect.
Запитуйте більше лише тоді, коли функція цього потребує, наприклад patient/MedicationStatement.rs для списку медикаментів, і поясніть причину у своєму додатку перед відкриттям екрана згоди.
Подальші кроки
- Власний бекенд. Якщо ваш продукт також має серверні функції (нагадування, панелі інструментів для клініцистів), див. Як створити медичний API за допомогою TypeScript. Зберігайте ключі API на сервері, а токени OAuth — на пристрої.
- Діаграми та AI-резюме.
anpheros.context.buildповертає контекст із джерелом для моделі: Як створити медичний додаток зі штучним інтелектом із бекендом FHIR. - Сирий FHIR.
anpheros.fhirнадає операції read, search, create, transaction,$everythingта$summaryдля того самого запису (Як зберігати медичні записи за допомогою FHIR R4).
Часті запитання
Чи існує FHIR SDK для Flutter?
Так. anpheros_sdk на pub.dev — це Dart-клієнт для Anpheros FHIR R4 та REST API, з підтримкою OAuth 2.1 / SMART on FHIR, PKCE, оновлення токенів, ідемпотентних записів та типізованих моделей. Він працює у Flutter на iOS, Android, macOS та вебі.
Де Flutter-додаток для охорони здоров'я має зберігати токени?
У безпечному сховищі платформи (Keychain на iOS, сховище на базі Keystore на Android) за допомогою такого пакету, як flutter_secure_storage, ніколи не в спільних налаштуваннях (shared preferences) або звичайних файлах.
Чи потрібен додатку власний бекенд?
Не для медичних даних: додаток читає та записує запис особи безпосередньо за її згодою. Вам потрібен бекенд для всього іншого, що робить ваш продукт, і для зберігання API-ключів, якщо ви створюєте записи від імені людей.
Чи можу я тестувати без реальних пацієнтів?
Так. Під час закритого бета-тестування OAuth-з'єднання використовують записи пісочниці (sandbox), а пісочниця містить 30 синтетичних пацієнтів з шестимісячною історією.
Пов'язане
- Як реалізувати згоду пацієнта за допомогою SMART on FHIR
- Створення медичного застосунку: ті самі будівельні блоки, з боку API
- Автентифікація та OAuth
- API медичних даних
- Безкоштовна база даних FHIR та пісочниця
Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card