Flutter离线优先FHIR客户端插件offline_first_fhir_client的使用

Flutter离线优先FHIR客户端插件offline_first_fhir_client的使用


介绍

“offline_first_fhir_client” 是一个用于在 Flutter 应用中实现离线优先数据同步功能的插件。该插件通过结合本地数据库(Hive)和远程服务器(如 HAPI FHIR),实现了数据的高效存储、管理和同步。

本文将详细介绍如何使用此插件,并提供完整的示例代码。


插件结构与依赖

包文件夹结构

(my_flutter_package)/
├── lib/
│   ├── src/
│   │   ├── api/
│   │   ├── syncing/
│   │   ├── database/
│   │   ├── conflict_resolution/
│   │   ├── encryption/
│   │   └── offline_first_fhir_client.dart
│   ├── offline_first_fhir_client.dart
├── example/
│   ├── lib/
│   │   └── main.dart
│   └── pubspec.yaml
├── test/
│   ├── api/
│   ├── database/
│   └── offline_first_fhir_client.dart
├── .gitignore
├── analysis_options.yaml
├── CHANGELOG.md
├── LICENSE
├── pubspec.lock
├── pubspec.yaml
└── README.md

关键依赖类

以下为插件的核心依赖类及其功能:

classDiagram
    direction LR
    class EncryptionHelper {
        +storeEncryptionKey()
        +retrieveEncryptionKey()
        +generateSecureKey()
    }
    class DatabaseController {
        +initializeHiveDatabase()
        +createData()
        +readData()
        +updateData()
        +deleteData()
        +resolveConflicts()
    }
    class SyncScheduler {
        -syncInterval: Duration
        +scheduleSyncOperation()
        +runPeriodicSync()
        +triggerManualSync()
    }
    class InternetChecker {
        +checkConnectivity()
        +monitorInternetStatus()
        +isInternetAvailable(): Boolean
    }
    class ApiService {
        +authorization()
        +postRequest()
        +putRequest()
        +deleteRequest()
        +getRequest()
        +updateRequest()
        +resolveApiConflicts()
    }
    EncryptionHelper --> DatabaseController: secure data storage
    DatabaseController --> SyncScheduler: data sync management
    SyncScheduler --> InternetChecker: connectivity verification
    InternetChecker --> ApiService: network status

功能概述

1. Hive 数据库

Hive 是一个轻量级的 NoSQL 数据库,用于本地存储数据。它支持加密存储,确保数据安全。

2. API 调用

ApiService 类负责所有 API 操作,包括授权、POST、PUT、DELETE 等请求。

3. 同步调度

SyncScheduler 类管理数据同步任务,支持手动和定时同步(例如每两天一次)。

4. 网络检测

InternetChecker 使用 connectivity_plus 库检查网络状态,确保同步操作在网络可用时执行。


示例代码

以下是一个完整的 Flutter 应用示例,展示如何使用 offline_first_fhir_client 插件进行离线优先数据同步。

示例代码:example/lib/main.dart

import 'package:example/edit_patient_data.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:offline_first_fhir_client/offline_first_fhir_client.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 初始化 FhirClient
  await FhirClient().initialize("https://fhir-server-persisted.wonderfulcliff-68cdbf49.westeurope.azurecontainerapps.io/hapi-fhir-jpaserver/fhir", "");

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Patient Data Manager',
      home: PatientInputScreen(),
    );
  }
}

class PatientInputScreen extends StatefulWidget {
  const PatientInputScreen({super.key});

  @override
  PatientInputScreenState createState() => PatientInputScreenState();
}

class PatientInputScreenState extends State<PatientInputScreen> {
  final _formKey = GlobalKey<FormState>();
  final _nameController = TextEditingController();
  final _birthDateController = TextEditingController();

  final fhirClient = FhirClient();

  final List<Map<String, dynamic>> _patients = [];
  bool isLoading = false;

  @override
  void dispose() {
    _nameController.dispose();
    _birthDateController.dispose();
    super.dispose();
  }

  @override
  void initState() {
    _getPatients();
    super.initState();
  }

  /// 获取所有患者数据并更新状态
  void _getPatients() async {
    _patients.clear();
    List<Map<String, dynamic>>? patientsData = await fhirClient.getAllResources("Patient");
    setState(() {
      _patients.addAll(patientsData);
    });
  }

  /// 添加新患者或更新现有患者
  Future<void> _addPatient() async {
    if (_formKey.currentState!.validate()) {
      final patient = {
        "id": DateTime.now().millisecondsSinceEpoch.toString(),
        "localVersionId": "1",
        "isSynced": false,
        "body": {
          "resourceType": "Patient",
          "name": [{"family": _nameController.text.split(" ").last, "given": [_nameController.text.split(" ").first]}],
          "birthDate": _birthDateController.text,
          "meta": {
            "versionId": "1",
            "lastUpdated": DateFormat("yyyy-MM-ddTHH:mm:ss.SSSXXX").format(DateTime.now().toUtc()),
          },
        },
      };
      setState(() {
        _patients.add(patient);
      });
      await _savePatientToLocal(patient);
      _clearFormInputs();
    }
  }

  /// 清空表单输入
  void _clearFormInputs() {
    _nameController.clear();
    _birthDateController.clear();
  }

  /// 将患者数据保存到本地数据库
  Future<void> _savePatientToLocal(Map<String, dynamic> patient) async {
    await fhirClient.saveResource("Patient", patient['id'], patient);
  }

  /// 同步所有患者数据到服务器
  Future<void> _syncPatientsToServer() async {
    setState(() {
      isLoading = true;
    });

    try {
      await fhirClient.syncResources(["Patient"], "/Patient/\$everything", {});
      _getPatients();
    } catch (e) {
      if (kDebugMode) {
        print("Sync error: $e");
      }
    } finally {
      setState(() {
        isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Patient Data Input')),
      body: Stack(
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                Form(
                  key: _formKey,
                  child: Column(
                    children: [
                      TextFormField(
                        controller: _nameController,
                        decoration: const InputDecoration(labelText: 'Name'),
                        validator: (value) {
                          if (value == null || value.isEmpty) {
                            return 'Please enter a name';
                          }
                          return null;
                        },
                      ),
                      TextFormField(
                        controller: _birthDateController,
                        decoration: const InputDecoration(labelText: 'Birth Date (YYYY-MM-DD)'),
                        validator: (value) {
                          if (value == null || value.isEmpty) {
                            return 'Please enter a birth date';
                          }
                          return null;
                        },
                      ),
                      const SizedBox(height: 20),
                      ElevatedButton(
                        onPressed: _addPatient,
                        child: const Text('Add Patient to Local'),
                      ),
                    ],
                  ),
                ),
                const SizedBox(height: 20),
                const Text('Patients From Local Database'),
                Expanded(
                  child: ListView.builder(
                    itemCount: _patients.length,
                    itemBuilder: (context, index) {
                      final patient = _patients[index];
                      return ListTile(
                        title: Text("${patient['body']['name'][0]['given'][0]} ${patient['body']['name'][0]['family']}"),
                        subtitle: Text('${patient['body']['birthDate']}'),
                        trailing: !patient['isSynced']
                            ? IconButton(
                                onPressed: () async {
                                  await fhirClient.deleteResourceByKey("Patient", patient['id']);
                                  setState(() {
                                    _patients.removeAt(index);
                                  });
                                },
                                icon: const Icon(Icons.delete, color: Colors.red),
                              )
                            : null,
                      );
                    },
                  ),
                ),
                const Text('New data will reflect on server within 5 mins'),
                ElevatedButton(
                  onPressed: isLoading ? null : _syncPatientsToServer,
                  child: Text(isLoading ? 'Syncing' : 'Sync All to Server'),
                ),
              ],
            ),
          ),
          isLoading ? const Center(child: CircularProgressIndicator()) : Container(),
        ],
      ),
    );
  }
}

运行步骤

  1. 初始化项目

    • 打开 Android Studio。
    • 选择 File > Open,导航到项目根目录并打开。
  2. 配置运行环境

    • 修改 baseUrlauthToken
      final apiService = ApiService(baseUrl: "http://192.168.1.8:8080/fhir", authToken: "your-auth-token");
      

更多关于Flutter离线优先FHIR客户端插件offline_first_fhir_client的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter离线优先FHIR客户端插件offline_first_fhir_client的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


offline_first_fhir_client 是一个 Flutter 插件,旨在帮助开发者构建支持离线优先的 FHIR 客户端应用。FHIR(Fast Healthcare Interoperability Resources)是一种用于医疗数据交换的标准,而离线优先意味着应用在没有网络连接的情况下仍然可以正常工作,并在网络恢复时同步数据。

主要功能

  1. 离线存储:在本地存储 FHIR 资源,以便在没有网络连接时访问。
  2. 数据同步:在网络恢复时,自动将本地数据与远程服务器同步。
  3. 缓存管理:管理缓存数据,确保数据的一致性和时效性。
  4. 错误处理:处理网络错误和同步冲突,确保数据的完整性。

安装

pubspec.yaml 文件中添加依赖:

dependencies:
  offline_first_fhir_client: ^0.1.0

然后运行 flutter pub get 来安装插件。

基本用法

  1. 初始化客户端: 首先,你需要初始化 OfflineFirstFhirClient,并指定 FHIR 服务器的 URL。

    import 'package:offline_first_fhir_client/offline_first_fhir_client.dart';
    
    final client = OfflineFirstFhirClient(
      baseUrl: 'https://your-fhir-server.com',
    );
    
  2. 获取资源: 使用 getResource 方法从服务器或本地缓存中获取 FHIR 资源。

    final patient = await client.getResource('Patient', '123');
    print(patient);
    
  3. 创建或更新资源: 使用 createResourceupdateResource 方法来创建或更新 FHIR 资源。这些操作会先在本地执行,然后在网络恢复时同步到服务器。

    final newPatient = Patient(
      id: '123',
      name: [HumanName(text: 'John Doe')],
    );
    
    await client.createResource(newPatient);
    
  4. 同步数据: 手动触发数据同步,以确保本地数据与服务器保持一致。

    await client.sync();
    
  5. 处理同步冲突: 在同步过程中可能会遇到冲突,你可以通过实现 ConflictResolver 来处理这些冲突。

    client.conflictResolver = MyConflictResolver();
    

示例代码

以下是一个简单的示例,展示了如何使用 offline_first_fhir_client 插件来获取和创建 FHIR 资源。

import 'package:flutter/material.dart';
import 'package:offline_first_fhir_client/offline_first_fhir_client.dart';
import 'package:fhir/r4.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  [@override](/user/override)
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final client = OfflineFirstFhirClient(
    baseUrl: 'https://your-fhir-server.com',
  );

  Patient? patient;

  [@override](/user/override)
  void initState() {
    super.initState();
    fetchPatient();
  }

  Future<void> fetchPatient() async {
    final fetchedPatient = await client.getResource('Patient', '123');
    setState(() {
      patient = fetchedPatient as Patient?;
    });
  }

  Future<void> createPatient() async {
    final newPatient = Patient(
      id: '124',
      name: [HumanName(text: 'Jane Doe')],
    );

    await client.createResource(newPatient);
    await client.sync();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Offline First FHIR Client'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (patient != null)
              Text('Patient: ${patient!.name?.first.text}'),
            ElevatedButton(
              onPressed: createPatient,
              child: Text('Create Patient'),
            ),
          ],
        ),
      ),
    );
  }
}
回到顶部