AnpherosAnpheros PlatformDevelopers

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

Flutter ile Sağlık Uygulaması Nasıl Oluşturulur

Öğretici: OAuth onayı ve PKCE ile bir kişinin FHIR R4 kaydına bağlanan, belirteçleri güvenli bir şekilde saklayan, kan basıncı okumalarını Dart SDK ile okuyup ekleyen ve erişim iptalini yöneten bir Flutter uygulaması.

Bu eğitim, bir kişinin HL7 FHIR R4 tıbbi kaydına onaylarıyla bağlanan, kan basıncı okumalarını gösteren ve yenilerini ekleyen bir Flutter uygulaması oluşturur; Dart SDK anpheros_sdk kullanılır. Uygulama, kişinin verdiği bir OAuth token'ı ile doğrudan Anpheros ile konuşur, API anahtarı ile değil, bu nedenle tıbbi veriler için kendi arka ucuna ihtiyaç duymaz. iOS ve Android'de çalışır; aynı kod, auth paketinin platform kurulumuyla macOS ve web'de de çalışır.

Parçalar Nasıl Bir Araya Geliyor

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

Bir mobil uygulama asla bir API anahtarı içermemelidir: Herkes onu çıkarabilir ve bir anahtar projenizin her hastasına ulaşır. OAuth ile uygulamadaki jeton, kişinin onayladığı yalnızca bir kayda ulaşır ve bu, kişi iptal edene kadar geçerlidir. Akış, SMART on FHIR ile hasta onayının nasıl uygulanacağı bölümünde açıklanan standart SMART on FHIR bağımsız başlatmadır; SDK protokol işini yapar.

Başlamadan Önce

Özel beta sırasında, OAuth bağlantıları sandbox kayıtlarını kullanır, böylece tüm akışı gerçek veriler olmadan test edebilirsiniz.

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

Android: android/app/src/main/AndroidManifest.xml içindeki <application>'de, şemanızla birlikte flutter_web_auth_2 geri çağırma etkinliğini ekleyin:

<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, özel bir şema için ek kurulum gerektirmez.

Adım 1: kayda bağlanma

OAuthFlow, bir PKCE meydan okumasıyla yetkilendirme adresini oluşturur; flutter_web_auth_2 bunu güvenli bir tarayıcı sekmesinde açar ve Anpheros bpapp://callback'e yönlendirdiğinde geri döner.

// 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()));
}

Onay ekranında kişi oturum açar, uygulamanızın adını, amacını ve tam olarak ne istediğini görür, kaydı (kendi kaydı, ilgilendiği bir kayıt veya uygulamanız için yeni bir kayıt) ve erişimin ne kadar süreceğini seçer. tokens.patient, uygulamanız için bu kaydın kimliğidir.

Yenileme belirteçleri döner: her yenileme yeni bir tane döndürür ve onTokens bunu hemen depolar. Eski bir yenileme belirtecini yeniden kullanmak izni geçersiz kılar, bu nedenle onTokens'i atlamayın.

Adım 2: Okumaları Oku

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

Okumalar, standart FHIR Observations olarak saklanır, LOINC 85354-9 sistolik ve diyastolik bileşenlerle birlikte, böylece kişinin doktoru, bir klinik sistemi veya bağladıkları başka bir uygulama, sizin uygulamanız hakkında hiçbir bilgi sahibi olmadan bunları okur. authorType: patient kişinin okumayı girdiğini kaydeder. SDK, her oluşturma işlemiyle birlikte bir idempotency anahtarı gönderir, böylece kötü bir bağlantı durumunda yeniden deneme okumayı iki kez eklemez.

Adım 3: Ekran

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

Bunu flutter run ile çalıştırın, Sağlık kaydımı bağla'ya dokunun, izin ekranında oturum açın, erişime izin verin ve ardından bir okuma ekleyin.

Kapsamlar ne işe yarar

Uygulama patient/Observation.rs?category=vital-signs ve patient/Observation.c istedi. Bu, yalnızca yaşamsal belirtileri okuyabileceği ve gözlemler ekleyebileceği, başka hiçbir şey yapamayacağı anlamına gelir. Kişinin laboratuvar sonuçları, teşhisleri veya belgeleri için bir istek 403'ü yanıtlar ve bu belirteç için başka herhangi bir hasta mevcut değildir (404). Kişi Anpheros'ta erişimi iptal ederse, bir sonraki çağrı 401'ü yanıtlar ve yukarıdaki ekran Connect'e geri döner.

Bir özellik gerektirdiğinde daha fazlasını isteyin, örneğin ilaç listesi için patient/MedicationStatement.rs, ve onay ekranını açmadan önce uygulamanızda nedenini belirtin.

İleriye Gitmek

Sıkça Sorulan Sorular

Flutter için bir FHIR SDK'sı var mı?

Evet. pub.dev'deki anpheros_sdk, Anpheros FHIR R4 ve REST API'leri için bir Dart istemcisidir. OAuth 2.1 / SMART on FHIR, PKCE, token yenileme, idempotent yazma ve tipli modelleri destekler. Flutter'da iOS, Android, macOS ve web üzerinde çalışır.

Bir Flutter sağlık uygulaması token'ları nereye kaydetmelidir?

Platformun güvenli depolama alanına (iOS'ta Keychain, Android'de Keystore destekli depolama) flutter_secure_storage gibi bir paket aracılığıyla kaydedilmelidir. Asla paylaşılan tercihlerde veya düz dosyalarda saklanmamalıdır.

Uygulamanın kendi arka ucuna ihtiyacı var mı?

Tıbbi veriler için gerekmez: uygulama, kişinin kaydını doğrudan kendi rızasıyla okur ve yazar. Ürününüzün yaptığı diğer her şey için ve kişilerin adına kayıt oluşturuyorsanız API anahtarlarını tutmak için bir arka uca ihtiyacınız olacaktır.

Gerçek hastalar olmadan test edebilir miyim?

Evet. Özel beta sırasında, OAuth bağlantıları sandbox kayıtlarını kullanır ve sandbox'ta altı aylık geçmişe sahip 30 sentetik hasta bulunur.

İlgili Konular

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