Flutter FHIR问卷处理插件fhir_questionnaire的使用
Flutter FHIR问卷处理插件fhir_questionnaire的使用
A Flutter包用于处理FHIR®问卷。FHIR®是HL7注册的商标,并且在获得HL7许可的情况下使用。使用FHIR商标并不构成对本产品的认可。
此包负责构建FHIR R4问卷的UI,处理行为和验证,并最终从用户答案生成问卷响应(QuestionnaireResponse)。
支持的问卷项
目前该包仅支持FHIR R4项目类型。
项目 | 支持 |
---|---|
Group | ✅ |
Display | ✅ |
Question | ☑️ |
Boolean | ✅ |
Decimal | ✅ |
Integer | ✅ |
Date | ✅ |
DateTime | ✅ |
Time | ✅ |
String | ✅ |
Text | ✅ |
Url | ✅ |
Choice | ✅ |
OpenChoice | ✅ |
Attachment | ✅ |
Reference | ☑️ |
Quantity | ✅ |
支持的额外功能
- 支持
enableWhen
- 支持
enableBehavior
- 支持计算表达式(Calculated Expression)
如何使用
只需将QuestionnaireView
小部件添加到您的小部件树中,即可拥有您的问卷UI。
QuestionnaireView(
questionnaire: questionnaire, // FHIR R4 Questionnaire实例
onAttachmentLoaded: onAttachmentLoaded, // 处理附件加载的回调
locale: locale, // 按钮和验证文本的语言
localizations: localizations, // 添加额外本地化的支持
isLoading: loading, // 是否有正在进行的操作
onSubmit: onSubmit, // 用户提交后的回调
controller: controller, // 用于项目的视图和响应生成的控制器
)
QuestionnaireView
参数说明
- Questionnaire questionnaire:
QuestionnaireView
需要一个类型为Questionnaire
的对象。这是问卷的定义,将用于构建表单UI并生成问题和答案。 - String? locale: 可选参数,可以指定您希望使用的语言,例如 “es” 或 “en” 或 “fr” 等。默认情况下,系统语言将被使用。
- List<QuestionnaireBaseLocalization> localizations: 这是一个列表,允许您向问卷添加额外的语言翻译。当前包支持英语和西班牙语,您可以添加其他语言。您只需要为每个新语言创建一个类并扩展
QuestionnaireBaseLocalization
。 - QuestionnaireBaseLocalization? defaultLocalization: 如果指定的语言或系统语言不受支持,这表示应使用什么作为回退本地化。默认情况下,英语是回退语言。
- bool isLoading: 使用此参数来指示是否有正在进行的操作。例如,如果您需要进行API请求以加载您的问卷,您可以设置
isLoading = true
,因此QuestionnaireView
将显示一个闪烁的加载效果视图。 - Future<Attachment?> Function()? onAttachmentLoaded: 为了使该包更简单并兼容所有受支持的Flutter平台,加载附件的功能已委托给应用程序。因此,您需要通过实现此函数并返回一个
Attachment
实例(根据FHIR规范)来处理此逻辑。 - ValueChanged: 这是在用户点击提交按钮后触发的回调,您将得到一个
QuestionnaireResponse
实例。您只需设置主题或其他您认为必要的额外数据,但答案将被覆盖。 - QuestionnaireController? controller: 这是在
QuestionnaireView
中用于项目的视图和响应生成的控制器。这里的目的在于允许您使用QuestionnaireController
扩展的一个实例,以便您可以覆盖行为和小部件。
一些额外注意事项
- 此小部件将使用应用程序的主题来构建,因此如果您想更改颜色、输入装饰等,只需在您的应用程序主题中更改即可。此外,包中的所有小部件都是公开的,可以暴露出来,如果有必要,您可以覆盖它们。
QuestionnaireView
实现会根据每个QuestionnaireItem
的定义来处理验证。- 请检查示例项目,它展示了所有功能的实际应用。
示例
以下是示例代码:
import 'dart:async';
import 'dart:convert';
import 'dart:ui';
import 'package:example/attachment_utils.dart';
import 'package:example/questionnaire_samples.dart';
import 'package:fhir/r4.dart';
import 'package:fhir_questionnaire/fhir_questionnaire.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
StreamController<InputDecorationTheme?> inputDecorationThemeStream =
StreamController<InputDecorationTheme>.broadcast();
class MyApp extends StatelessWidget {
const MyApp({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
return StreamBuilder<InputDecorationTheme?>(
stream: inputDecorationThemeStream.stream,
initialData: null,
builder: (context, snapshot) {
return MaterialApp(
title: 'FHIR Questionnaire Demo',
scrollBehavior: const CustomScrollBehavior(),
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: true,
inputDecorationTheme: snapshot.data),
home: const MyHomePage(),
);
},
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
[@override](/user/override)
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final List<({String name, String? value})> locales = [
(name: 'System default', value: null),
(name: 'English', value: 'en'),
(name: 'Spanish', value: 'es'),
(name: 'French', value: 'fr'),
];
final List<({String name, int value})> questionnaires = [
(name: 'Generic', value: 0),
(name: 'PRAPARE', value: 1),
(name: 'PHQ-9', value: 2),
(name: 'GAD-7', value: 3),
(name: 'BMI', value: 4),
];
final List<({String name, InputDecorationTheme? value})> inputDecorationThemes = [
(name: 'Default', value: null),
(
name: 'Outline',
value: const InputDecorationTheme(
contentPadding: EdgeInsets.only(left: 16, top: 12, bottom: 12),
border: OutlineInputBorder(),
)
),
(
name: 'Outline Streched Rounded',
value: const InputDecorationTheme(
contentPadding: EdgeInsets.only(left: 16, top: 12, bottom: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(28)),
),
)
),
(
name: 'Outline Streched Rounded Filled',
value: const InputDecorationTheme(
contentPadding: EdgeInsets.only(left: 16, top: 12, bottom: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(28)),
),
filled: true,
)
),
(
name: 'Outline Streched Rounded Filled No Borders',
value: const InputDecorationTheme(
contentPadding: EdgeInsets.only(left: 16, top: 12, bottom: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(28)),
borderSide: BorderSide(
style: BorderStyle.none,
width: 0,
),
),
isDense: true,
alignLabelWithHint: true,
filled: true,
)
),
];
String? selectedLocale;
int selectedQuestionnaire = 0;
InputDecorationTheme? selectedInputDecorationTheme;
final extraLocalizations = [QuestionnaireFrLocalization()];
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('FHIR Questionnaire Demo'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DropdownButtonFormField<int>(
decoration: const InputDecoration(
label: Text('Select a Questionnaire sample')),
value: selectedQuestionnaire,
items: questionnaires
.map((e) => DropdownMenuItem<int>(
value: e.value,
child: Text(e.name),
))
.toList(),
onChanged: (value) {
if (value != null) {
selectedQuestionnaire = value;
}
}),
const SizedBox(height: 16.0),
DropdownButtonFormField<String?>(
decoration: const InputDecoration(
label: Text('Select the Questionnaire locale')),
value: selectedLocale,
items: locales
.map((e) => DropdownMenuItem<String>(
value: e.value,
child: Text(e.name),
))
.toList(),
onChanged: (value) {
selectedLocale = value;
}),
const SizedBox(height: 16.0),
DropdownButtonFormField<InputDecorationTheme?>(
decoration: const InputDecoration(
label: Text('Select input decoration theme ')),
value: selectedInputDecorationTheme,
items: inputDecorationThemes
.map((e) => DropdownMenuItem<InputDecorationTheme>(
value: e.value,
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.78,
child: Text(e.name),
),
))
.toList(),
onChanged: (value) {
selectedInputDecorationTheme = value;
inputDecorationThemeStream.add(selectedInputDecorationTheme);
}),
],
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: FloatingActionButton.extended(
extendedPadding: const EdgeInsets.symmetric(horizontal: 32.0),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => QuestionnairePage(
questionnaire: questionnaire,
locale: selectedLocale,
localizations: extraLocalizations,
),
));
},
label: const Text('Open Questionnaire'),
),
);
}
Questionnaire get questionnaire =>
Questionnaire.fromJsonString(switch (selectedQuestionnaire) {
1 => QuestionnaireSamples.samplePrapare,
2 => QuestionnaireSamples.samplePHQ9,
3 => QuestionnaireSamples.sampleGAD7,
4 => QuestionnaireSamples.sampleBMI,
0 || _ => QuestionnaireSamples.sampleGeneric,
});
}
class QuestionnairePage extends StatefulWidget {
final Questionnaire questionnaire;
final String? locale;
final List<QuestionnaireBaseLocalization>? localizations;
const QuestionnairePage({
super.key,
required this.questionnaire,
this.locale,
this.localizations,
});
[@override](/user/override)
State<StatefulWidget> createState() => QuestionnairePageState();
}
class QuestionnairePageState extends State<QuestionnairePage> {
bool loading = true;
[@override](/user/override)
void initState() {
super.initState();
Future.delayed(
const Duration(seconds: 1), () => setState(() => loading = false));
}
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Questionnaire'),
),
body: QuestionnaireView(
key: ValueKey(loading),
questionnaire: widget.questionnaire,
onAttachmentLoaded: onAttachmentLoaded,
locale: widget.locale,
localizations: widget.localizations,
isLoading: loading,
onSubmit: onSubmit,
),
);
}
Future<Attachment?> onAttachmentLoaded() async {
return AttachmentUtils.pickAttachment(context);
}
void onSubmit(QuestionnaireResponse questionnaireResponse) async {
Navigator.pop(context);
var prettyString = const JsonEncoder.withIndent(' ')
.convert(questionnaireResponse.toJson());
debugPrint('''
========================================================================
$prettyString
========================================================================
''');
}
}
/// 法语本地化
class QuestionnaireFrLocalization extends QuestionnaireBaseLocalization {
QuestionnaireFrLocalization() : super('fr');
[@override](/user/override)
String get btnSubmit => 'Soumettre';
[@override](/user/override)
String get btnUpload => 'Télécharger';
[@override](/user/override)
String get btnChange => 'Changement';
[@override](/user/override)
String get btnRemove => 'Retirer';
[@override](/user/override)
String get textOtherOption => 'Autre option';
[@override](/user/override)
String get textDate => 'Date';
[@override](/user/override)
String get textTime => 'Temps';
[@override](/user/override)
String get exceptionNoEmptyField => 'Ce champ est obligatoire.';
[@override](/user/override)
String get exceptionValueMustBeAPositiveIntegerNumber =>
'La valeur doit être un nombre entier positif.';
[@override](/user/override)
String get exceptionValueMustBeAPositiveNumber =>
'La valeur doit être un nombre positif.';
[@override](/user/override)
String get exceptionInvalidUrl => 'Invalid url.';
[@override](/user/override)
String exceptionValueOutOfRange(dynamic minValue, dynamic maxValue) =>
'La valeur doit être comprise entre $minValue et $maxValue.';
[@override](/user/override)
String exceptionTextLength(dynamic minLength, dynamic maxLength) =>
'Le texte doit contenir au moins des caractères $minLength et au maximum $maxLength.';
[@override](/user/override)
String exceptionTextMaxLength(dynamic maxLength) =>
'Le texte doit contenir au maximum des caractères $maxLength.';
}
class CustomScrollBehavior extends MaterialScrollBehavior {
static const _webScrollPhysics =
BouncingScrollPhysics(parent: RangeMaintainingScrollPhysics());
const CustomScrollBehavior() : super();
// 覆盖行为方法和获取器,如dragDevices
[@override](/user/override)
Set<PointerDeviceKind> get dragDevices => PointerDeviceKind.values.toSet();
[@override](/user/override)
ScrollPhysics getScrollPhysics(BuildContext context) =>
kIsWeb ? _webScrollPhysics : super.getScrollPhysics(context);
}
更多关于Flutter FHIR问卷处理插件fhir_questionnaire的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter FHIR问卷处理插件fhir_questionnaire的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
在处理Flutter应用中的FHIR问卷时,fhir_questionnaire
插件是一个非常有用的工具。这个插件允许你根据FHIR(Fast Healthcare Interoperability Resources)标准创建和管理问卷。以下是一个如何使用fhir_questionnaire
插件的基本示例代码,展示了如何加载问卷、渲染问卷以及收集用户输入。
首先,确保你已经在pubspec.yaml
文件中添加了fhir
和fhir_questionnaire
依赖:
dependencies:
flutter:
sdk: flutter
fhir: ^x.y.z # 替换为最新版本号
fhir_questionnaire: ^x.y.z # 替换为最新版本号
然后,运行flutter pub get
来安装依赖。
接下来,是一个简单的Flutter应用示例,展示了如何使用fhir_questionnaire
插件:
import 'package:flutter/material.dart';
import 'package:fhir/r4.dart' as fhir;
import 'package:fhir_questionnaire/fhir_questionnaire.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter FHIR Questionnaire Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: QuestionnaireScreen(),
);
}
}
class QuestionnaireScreen extends StatefulWidget {
@override
_QuestionnaireScreenState createState() => _QuestionnaireScreenState();
}
class _QuestionnaireScreenState extends State<QuestionnaireScreen> {
late fhir.Questionnaire questionnaire;
late QuestionnaireResponseController controller;
@override
void initState() {
super.initState();
// 加载或定义你的FHIR问卷
questionnaire = fhir.Questionnaire.fromJson({
'resourceType': 'Questionnaire',
'id': 'example',
'status': 'active',
'item': [
{
'linkId': '1',
'text': 'What is your name?',
'type': 'string',
},
{
'linkId': '2',
'text': 'How old are you?',
'type': 'integer',
},
// 添加更多问题...
],
});
// 初始化控制器
controller = QuestionnaireResponseController(questionnaire);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FHIR Questionnaire Demo'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: QuestionnaireWidget(
questionnaire: questionnaire,
controller: controller,
onSubmit: (response) {
// 处理提交的问卷响应
print('Submitted response: $response');
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 提交问卷
controller.submit();
},
tooltip: 'Submit',
child: Icon(Icons.send),
),
);
}
}
在这个示例中:
-
定义问卷:在
initState
方法中,我们定义了一个简单的FHIR问卷,包含两个问题:一个是字符串类型(名字),另一个是整数类型(年龄)。 -
初始化控制器:使用
QuestionnaireResponseController
来管理问卷的响应。 -
渲染问卷:使用
QuestionnaireWidget
来渲染问卷,并传入问卷定义和控制器。 -
提交问卷:当用户点击浮动按钮时,调用
controller.submit()
方法提交问卷,并在onSubmit
回调中处理提交的响应。
请注意,这个示例仅展示了基本的问卷创建和提交流程。在实际应用中,你可能需要从服务器加载问卷定义,处理更复杂的问卷逻辑,以及将问卷响应存储到后端服务中。fhir
和fhir_questionnaire
插件提供了丰富的API来满足这些需求,你可以查阅它们的文档以了解更多高级用法。