# Flutter ile Sağlık Uygulaması Oluşturma

> Eğitim: OAuth onayı ve PKCE ile bir kişinin FHIR R4 kaydına bağlanan, token'ları güvenli bir şekilde saklayan, kan basıncı okumalarını Dart SDK ile okuyup ekleyen ve erişim iptalini yöneten bir Flutter uygulaması.

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

**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ğı](https://developers.anpheros.com/tr/guides/smart-on-fhir-backend) bölümünde açıklanan standart SMART on FHIR bağımsız başlatmadır; SDK protokol işini yapar.

## Başlamadan Önce

- Flutter 3.22 veya daha yenisi.
- Bir Anpheros projesi. [platform.anpheros.com/dashboard](https://platform.anpheros.com/dashboard/) adresinden Google ile giriş yapın ve bir sandbox anahtarı alın; proje bununla birlikte gelir.
- Dashboard'da, yönlendirme URI'si `bpapp://callback` olan **public** türünde bir uygulama kaydedin. `client_id`'unu not edin.

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

```bash
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:

```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, ö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.

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

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

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

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

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

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

- **Kendi arka ucunuz.** Ürününüz aynı zamanda sunucu özelliklerine (hatırlatıcılar, klinisyen panoları) sahipse, [TypeScript ile Sağlık API'si Nasıl Oluşturulur](https://developers.anpheros.com/tr/guides/healthcare-api-typescript) bölümüne bakın. API anahtarlarını sunucuda ve OAuth belirteçlerini cihazda saklayın.
- **Grafikler ve Yapay Zeka özetleri.** `anpheros.context.build`, bir model için kaynak etiketli bir bağlam döndürür: [FHIR arka ucu ile Yapay Zeka Sağlık Uygulaması Nasıl Oluşturulur](https://developers.anpheros.com/tr/guides/build-ai-healthcare-app).
- **Ham FHIR.** `anpheros.fhir`, aynı kayıt üzerinde okuma, arama, oluşturma, işlem, `$everything` ve `$summary` sağlar ([FHIR R4 Kullanarak Tıbbi Kayıtlar Nasıl Saklanır](https://developers.anpheros.com/tr/guides/store-medical-records-fhir)).

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

- [SMART on FHIR ile hasta rızasını uygulama](https://developers.anpheros.com/tr/guides/smart-on-fhir-backend)
- [Sağlık uygulaması oluşturma](https://developers.anpheros.com/tr/guides/build-a-healthcare-app): API tarafında aynı yapı taşları
- [Kimlik Doğrulama ve OAuth](https://developers.anpheros.com/tr/guides/authentication)
- [Tıbbi Veri API'si](https://developers.anpheros.com/tr/guides/medical-data-api)
- [Ücretsiz FHIR veritabanı ve sandbox](https://developers.anpheros.com/tr/guides/free-fhir-database)
