diff --git a/example/lib/custom_questionnaire_stepper_page.dart b/example/lib/custom_questionnaire_stepper_page.dart
index cc1d9910..44bb981e 100644
--- a/example/lib/custom_questionnaire_stepper_page.dart
+++ b/example/lib/custom_questionnaire_stepper_page.dart
@@ -1,4 +1,5 @@
import 'package:faiadashu/faiadashu.dart';
+import 'package:faiadashu_example/main.dart';
import 'package:flutter/material.dart';
class CustomQuestionnaireStepperPage extends StatefulWidget {
@@ -41,7 +42,8 @@ class _CustomQuestionnaireStepperPageState
}
// Validate the first matching item, and if it's valid, navigate to the next page
- if (matchingItems.first.validate(notifyListeners: true) == null) {
+ final errors = matchingItems.first.validate(notifyListeners: true);
+ if (errors.isEmpty) {
_navigateToNextPage();
}
}
@@ -67,8 +69,39 @@ class _CustomQuestionnaireStepperPageState
children: [
Expanded(
child: QuestionnaireStepper(
- scaffoldBuilder:
- const DefaultQuestionnairePageScaffoldBuilder(),
+ scaffoldBuilder: DefaultQuestionnairePageScaffoldBuilder(
+ persistentFooterButtons: [
+ PopupMenuButton(
+ icon: const Icon(Icons.language),
+ onSelected: (Locale locale) {
+ LocaleInheritedWidget.of(context)
+ .updateLocale(locale);
+ },
+ itemBuilder: (BuildContext context) =>
+ >[
+ const PopupMenuItem(
+ value: Locale('en'),
+ child: Text('English'),
+ ),
+ const PopupMenuItem(
+ value: Locale('ar'),
+ child: Text('عَرَبِيّ'),
+ ),
+ const PopupMenuItem(
+ value: Locale('de'),
+ child: Text('Deutsch'),
+ ),
+ const PopupMenuItem(
+ value: Locale('es'),
+ child: Text('Español'),
+ ),
+ const PopupMenuItem(
+ value: Locale('ja'),
+ child: Text('日本語'),
+ ),
+ ],
+ ),
+ ]),
fhirResourceProvider: widget.fhirResourceProvider,
launchContext: widget.launchContext,
controller: _controller,
@@ -77,8 +110,6 @@ class _CustomQuestionnaireStepperPageState
},
onPageChanged: _onPageChanged,
onBeforePageChanged: (currentItemModel, nextItemModel) async {
- /// Adding delay
- await Future.delayed(Duration(seconds: 1));
return BeforePageChangedData(canProceed: true);
},
onVisibleItemUpdated: (item) {
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 0019d3c9..29851e01 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -44,32 +44,73 @@ void main() {
runApp(const MyApp());
}
-class MyApp extends StatelessWidget {
+class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
+ @override
+ MyAppState createState() => MyAppState();
+}
+
+class MyAppState extends State {
+ Locale _currentLocale = const Locale('en', 'US');
+
+ void _updateLocale(Locale newLocale) {
+ setState(() {
+ _currentLocale = newLocale;
+ });
+ }
+
@override
Widget build(BuildContext context) {
- return MaterialApp(
- debugShowCheckedModeBanner: false,
- theme: ThemeData.light(useMaterial3: true).copyWith(
- colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink),
- inputDecorationTheme: ThemeData.light(useMaterial3: true).inputDecorationTheme.copyWith(
- filled: true,
- ),
+ return LocaleInheritedWidget(
+ updateLocale: _updateLocale,
+ currentLocale: _currentLocale,
+ child: MaterialApp(
+ debugShowCheckedModeBanner: false,
+ theme: ThemeData.light(useMaterial3: true).copyWith(
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink),
+ inputDecorationTheme:
+ ThemeData.light(useMaterial3: true).inputDecorationTheme.copyWith(
+ filled: true,
+ ),
+ ),
+ darkTheme: ThemeData.dark(useMaterial3: true),
+ title: 'Faiadashu™ FHIRDash Gallery',
+ localizationsDelegates: const [
+ FDashLocalizations.delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ ],
+ supportedLocales: FDashLocalizations.supportedLocales,
+ locale: _currentLocale,
+ home: const HomePage(),
),
- darkTheme: ThemeData.dark(useMaterial3: true),
- title: 'Faiadashu™ FHIRDash Gallery',
- localizationsDelegates: const [
- FDashLocalizations.delegate,
- GlobalMaterialLocalizations.delegate,
- GlobalWidgetsLocalizations.delegate,
- ],
- supportedLocales: FDashLocalizations.supportedLocales,
- home: const HomePage(),
);
}
}
+class LocaleInheritedWidget extends InheritedWidget {
+ final Function(Locale) updateLocale;
+ final Locale currentLocale;
+
+ const LocaleInheritedWidget({
+ Key? key,
+ required this.updateLocale,
+ required this.currentLocale,
+ required Widget child,
+ }) : super(key: key, child: child);
+
+ @override
+ bool updateShouldNotify(LocaleInheritedWidget old) {
+ return old.currentLocale != currentLocale;
+ }
+
+ static LocaleInheritedWidget of(BuildContext context) {
+ return context.dependOnInheritedWidgetOfExactType()!;
+ }
+}
+
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@@ -123,7 +164,6 @@ class _HomePageState extends State {
clientId: '4564f6f7-335f-43d3-8867-a0f4e6f901d6',
redirectUri: FhirUri('com.legentix.faiagallery://callback'),
);
-
}
@override
@@ -134,7 +174,8 @@ class _HomePageState extends State {
/// Schedules repaint after login / logout.
void _onLoginChanged() {
- _logger.debug('_onLoginChanged: ${questionnaireResponseStorage.smartClient.isLoggedIn()}');
+ _logger.debug(
+ '_onLoginChanged: ${questionnaireResponseStorage.smartClient.isLoggedIn()}');
setState(() {
// Rebuild
});
@@ -206,7 +247,8 @@ class _HomePageState extends State {
),
),
actions: [
- SmartLoginButton(questionnaireResponseStorage.smartClient, onLoginChanged: _onLoginChanged)
+ SmartLoginButton(questionnaireResponseStorage.smartClient,
+ onLoginChanged: _onLoginChanged)
],
),
body: SafeArea(
@@ -225,7 +267,8 @@ class _HomePageState extends State {
launchContext: launchContext,
questionnairePath: 'assets/instruments/beverage_ig.json',
saveResponseFunction: questionnaireResponseStorage.saveToMemory,
- restoreResponseFunction: questionnaireResponseStorage.restoreFromMemory,
+ restoreResponseFunction:
+ questionnaireResponseStorage.restoreFromMemory,
uploadResponseFunction: uploadResponseFunction,
questionnaireModelDefaults: QuestionnaireModelDefaults(
prefixBuilder: (fim) {
@@ -396,7 +439,8 @@ class _HomePageState extends State {
launchContext: launchContext,
questionnairePath: 'assets/instruments/bluebook.json',
saveResponseFunction: questionnaireResponseStorage.saveToMemory,
- restoreResponseFunction: questionnaireResponseStorage.restoreFromMemory,
+ restoreResponseFunction:
+ questionnaireResponseStorage.restoreFromMemory,
uploadResponseFunction: uploadResponseFunction,
),
_launchQuestionnaire(
@@ -425,7 +469,8 @@ class _HomePageState extends State {
launchContext: launchContext,
questionnairePath: 'assets/instruments/argonaut_sampler.json',
saveResponseFunction: questionnaireResponseStorage.saveToMemory,
- restoreResponseFunction: questionnaireResponseStorage.restoreFromMemory,
+ restoreResponseFunction:
+ questionnaireResponseStorage.restoreFromMemory,
uploadResponseFunction: uploadResponseFunction,
),
QuestionnaireLaunchTile(
@@ -439,7 +484,8 @@ class _HomePageState extends State {
launchContext: launchContext,
questionnairePath: 'assets/instruments/argonaut_sampler.json',
saveResponseFunction: questionnaireResponseStorage.saveToMemory,
- restoreResponseFunction: questionnaireResponseStorage.restoreFromMemory,
+ restoreResponseFunction:
+ questionnaireResponseStorage.restoreFromMemory,
uploadResponseFunction: uploadResponseFunction,
),
QuestionnaireLaunchTile(
@@ -453,7 +499,8 @@ class _HomePageState extends State {
launchContext: launchContext,
questionnairePath: 'assets/instruments/argonaut_sampler.json',
saveResponseFunction: questionnaireResponseStorage.saveToMemory,
- restoreResponseFunction: questionnaireResponseStorage.restoreFromMemory,
+ restoreResponseFunction:
+ questionnaireResponseStorage.restoreFromMemory,
uploadResponseFunction: uploadResponseFunction,
),
_headline(
diff --git a/example/lib/observation_page.dart b/example/lib/observation_page.dart
index c103a23d..55ffb8cf 100644
--- a/example/lib/observation_page.dart
+++ b/example/lib/observation_page.dart
@@ -119,12 +119,18 @@ class ObservationPage extends ExhibitPage {
const SizedBox(
height: 16,
),
- ObservationView(
- bpObservationWHR,
- valueStyle: Theme.of(context).textTheme.headline4,
- codeStyle: Theme.of(context).textTheme.subtitle2,
- dateTimeStyle: Theme.of(context).textTheme.caption,
- locale: const Locale.fromSubtags(languageCode: 'ar', countryCode: 'BH'),
+ Localizations.override(
+ context: context,
+ locale: const Locale.fromSubtags(
+ languageCode: 'ar',
+ countryCode: 'BH',
+ ),
+ child: ObservationView(
+ bpObservationWHR,
+ valueStyle: Theme.of(context).textTheme.headline4,
+ codeStyle: Theme.of(context).textTheme.subtitle2,
+ dateTimeStyle: Theme.of(context).textTheme.caption,
+ ),
),
],
);
diff --git a/example/lib/primitive_page.dart b/example/lib/primitive_page.dart
index d267357e..dcb1e356 100644
--- a/example/lib/primitive_page.dart
+++ b/example/lib/primitive_page.dart
@@ -17,25 +17,49 @@ class PrimitivePage extends ExhibitPage {
FhirDateTimeText(FhirDateTime('2010-02')),
const Spacer(),
Text('Germany', style: Theme.of(context).textTheme.headline6),
- FhirDateTimeText(
- FhirDateTime('2010-02-05 14:02'),
- locale: const Locale.fromSubtags(languageCode: 'de', countryCode: 'DE'),
+ Localizations.override(
+ context: context,
+ locale: const Locale.fromSubtags(
+ languageCode: 'de',
+ countryCode: 'DE',
+ ),
+ child: FhirDateTimeText(
+ FhirDateTime('2010-02-05 14:02'),
+ ),
),
const Spacer(),
Text('Japan', style: Theme.of(context).textTheme.headline6),
- FhirDateTimeText(
- FhirDateTime('2010-02'),
- locale: const Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'),
+ Localizations.override(
+ context: context,
+ locale: const Locale.fromSubtags(
+ languageCode: 'ja',
+ countryCode: 'JP',
+ ),
+ child: FhirDateTimeText(
+ FhirDateTime('2010-02'),
+ ),
),
- FhirDateTimeText(
- FhirDateTime('2010-02-05 14:02'),
- locale: const Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'),
+ Localizations.override(
+ context: context,
+ locale: const Locale.fromSubtags(
+ languageCode: 'ja',
+ countryCode: 'JP',
+ ),
+ child: FhirDateTimeText(
+ FhirDateTime('2010-02-05 14:02'),
+ ),
),
const Spacer(),
Text('Bahrain', style: Theme.of(context).textTheme.headline6),
- FhirDateTimeText(
- FhirDateTime('2010-02-05 14:02'),
- locale: const Locale.fromSubtags(languageCode: 'ar', countryCode: 'BH'),
+ Localizations.override(
+ context: context,
+ locale: const Locale.fromSubtags(
+ languageCode: 'ar',
+ countryCode: 'BH',
+ ),
+ child: FhirDateTimeText(
+ FhirDateTime('2010-02-05 14:02'),
+ ),
),
],
);
diff --git a/example/lib/questionnaire_launch_tile.dart b/example/lib/questionnaire_launch_tile.dart
index d8f04e96..a296285a 100644
--- a/example/lib/questionnaire_launch_tile.dart
+++ b/example/lib/questionnaire_launch_tile.dart
@@ -15,11 +15,12 @@ class QuestionnaireLaunchTile extends StatefulWidget {
final Locale? locale;
final FhirResourceProvider fhirResourceProvider;
final LaunchContext launchContext;
- final void Function(String questionnairePath, QuestionnaireResponse? questionnaireResponse)
- saveResponseFunction;
- final void Function(BuildContext context, String questionnairePath, QuestionnaireResponse? questionnaireResponse)?
- uploadResponseFunction;
- final QuestionnaireResponse? Function(String questionnairePath) restoreResponseFunction;
+ final void Function(String questionnairePath,
+ QuestionnaireResponse? questionnaireResponse) saveResponseFunction;
+ final void Function(BuildContext context, String questionnairePath,
+ QuestionnaireResponse? questionnaireResponse)? uploadResponseFunction;
+ final QuestionnaireResponse? Function(String questionnairePath)
+ restoreResponseFunction;
final QuestionnaireModelDefaults questionnaireModelDefaults;
@@ -45,7 +46,6 @@ class QuestionnaireLaunchTile extends StatefulWidget {
class _QuestionnaireLaunchTileState extends State {
late final FhirResourceProvider _questionnaireProvider;
- late Locale _locale;
late NumberFormat _percentPattern;
late Future _modelFuture;
@@ -58,11 +58,14 @@ class _QuestionnaireLaunchTileState extends State {
);
}
- Future _createModelFuture() {
+ Future _createModelFuture(
+ BuildContext context,
+ ) {
return QuestionnaireResponseModel.fromFhirResourceBundle(
+ localizations: FDashLocalizations.of(context),
fhirResourceProvider: _questionnaireProvider,
launchContext: widget.launchContext,
- locale: _locale,
+ locale: Localizations.localeOf(context),
).then((qrm) {
qrm.populate(
widget.restoreResponseFunction.call(widget.questionnairePath),
@@ -74,9 +77,10 @@ class _QuestionnaireLaunchTileState extends State {
@override
void didChangeDependencies() {
super.didChangeDependencies();
- _locale = widget.locale ?? Localizations.localeOf(context);
- _percentPattern = NumberFormat.percentPattern(_locale.toString());
- _modelFuture = _createModelFuture();
+ _percentPattern = NumberFormat.percentPattern(
+ Localizations.localeOf(context).toString(),
+ );
+ _modelFuture = _createModelFuture(context);
}
@override
@@ -134,62 +138,92 @@ class _QuestionnaireLaunchTileState extends State {
Navigator.push(
context,
MaterialPageRoute(
- builder: (context) => QuestionnaireScrollerPage(
- locale: _locale,
- fhirResourceProvider: RegistryFhirResourceProvider([
- AssetResourceProvider.singleton(
- questionnaireResourceUri,
- widget.questionnairePath,
- ),
- InMemoryResourceProvider.inMemory(
- questionnaireResponseResourceUri,
- widget.restoreResponseFunction(
+ builder: (context) => Localizations.override(
+ context: context,
+ locale: widget.locale,
+ child: QuestionnaireScrollerPage(
+ fhirResourceProvider: RegistryFhirResourceProvider([
+ AssetResourceProvider.singleton(
+ questionnaireResourceUri,
widget.questionnairePath,
),
- ),
- widget.fhirResourceProvider,
- ]),
- launchContext: widget.launchContext,
- // Callback for supportLink
- onLinkTap: launchLink,
- persistentFooterButtons: [
- Builder(
- builder: (context) => const QuestionnaireCompleteButton(),
- ),
- if (widget.uploadResponseFunction != null)
+ InMemoryResourceProvider.inMemory(
+ questionnaireResponseResourceUri,
+ widget.restoreResponseFunction(
+ widget.questionnairePath,
+ ),
+ ),
+ widget.fhirResourceProvider,
+ ]),
+ launchContext: widget.launchContext,
+ // Callback for supportLink
+ onLinkTap: launchLink,
+ persistentFooterButtons: [
+ Builder(
+ builder: (context) => const QuestionnaireCompleteButton(),
+ ),
+ if (widget.uploadResponseFunction != null)
+ Builder(
+ builder: (context) => ElevatedButton.icon(
+ label: Text(
+ FDashLocalizations.of(context)
+ .handlingUploadButtonLabel,
+ ),
+ icon: const Icon(Icons.cloud_upload),
+ onPressed: () {
+ // Generate a response and upload it to a FHIR server.
+ // In a real-world scenario this would have more robust state handling.
+ widget.uploadResponseFunction?.call(
+ context,
+ widget.questionnairePath,
+ QuestionnaireResponseFiller.of(context)
+ .aggregator()
+ .aggregate(
+ responseStatus:
+ QuestionnaireResponseStatus.completed,
+ ),
+ );
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Row(
+ mainAxisAlignment:
+ MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ FDashLocalizations.of(context)
+ .handlingUploading,
+ ),
+ SyncIndicator(
+ color:
+ Theme.of(context).colorScheme.primary,
+ )
+ ],
+ ),
+ ),
+ );
+ Navigator.pop(context);
+ },
+ ),
+ ),
Builder(
builder: (context) => ElevatedButton.icon(
label: Text(
- FDashLocalizations.of(context)
- .handlingUploadButtonLabel,
+ FDashLocalizations.of(context).handlingSaveButtonLabel,
),
- icon: const Icon(Icons.cloud_upload),
+ icon: const Icon(Icons.save_alt),
onPressed: () {
- // Generate a response and upload it to a FHIR server.
- // In a real-world scenario this would have more robust state handling.
- widget.uploadResponseFunction?.call(
- context,
+ // Generate a response and store it in-memory.
+ // In a real-world scenario one would persist or post the response instead.
+ widget.saveResponseFunction.call(
widget.questionnairePath,
QuestionnaireResponseFiller.of(context)
.aggregator()
- .aggregate(
- responseStatus:
- QuestionnaireResponseStatus.completed,
- ),
+ .aggregate(),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
- content: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- FDashLocalizations.of(context)
- .handlingUploading,
- ),
- SyncIndicator(
- color: Theme.of(context).colorScheme.primary,
- )
- ],
+ content: Text(
+ FDashLocalizations.of(context).handlingSaved,
),
),
);
@@ -197,40 +231,15 @@ class _QuestionnaireLaunchTileState extends State {
},
),
),
- Builder(
- builder: (context) => ElevatedButton.icon(
- label: Text(
- FDashLocalizations.of(context).handlingSaveButtonLabel,
- ),
- icon: const Icon(Icons.save_alt),
- onPressed: () {
- // Generate a response and store it in-memory.
- // In a real-world scenario one would persist or post the response instead.
- widget.saveResponseFunction.call(
- widget.questionnairePath,
- QuestionnaireResponseFiller.of(context)
- .aggregator()
- .aggregate(),
- );
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(
- FDashLocalizations.of(context).handlingSaved,
- ),
- ),
- );
- Navigator.pop(context);
- },
- ),
- ),
- ],
- questionnaireModelDefaults: widget.questionnaireModelDefaults,
+ ],
+ questionnaireModelDefaults: widget.questionnaireModelDefaults,
+ ),
),
),
).then((value) {
// This triggers after return from questionnaire filler
setState(() {
- _modelFuture = _createModelFuture();
+ _modelFuture = _createModelFuture(context);
});
});
},
diff --git a/lib/fhir_types/src/codeable_concept_text.dart b/lib/fhir_types/src/codeable_concept_text.dart
index 495a72a1..0e0e5b29 100644
--- a/lib/fhir_types/src/codeable_concept_text.dart
+++ b/lib/fhir_types/src/codeable_concept_text.dart
@@ -5,20 +5,17 @@ import 'package:flutter/material.dart';
class CodeableConceptText extends StatelessWidget {
final CodeableConcept codeableConcept;
final TextStyle? style;
- final Locale? locale;
const CodeableConceptText(
this.codeableConcept, {
this.style,
- this.locale,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
- codeableConcept
- .localizedDisplay(locale ?? Localizations.localeOf(context)),
+ codeableConcept.localizedDisplay(Localizations.localeOf(context)),
style: style,
);
}
diff --git a/lib/fhir_types/src/date_time_picker.dart b/lib/fhir_types/src/date_time_picker.dart
index f1bf5769..d84139f9 100644
--- a/lib/fhir_types/src/date_time_picker.dart
+++ b/lib/fhir_types/src/date_time_picker.dart
@@ -10,7 +10,6 @@ import 'package:intl/intl.dart';
/// The control is displayed as a text field that can be tapped to open
/// a picker.
class FhirDateTimePicker extends StatefulWidget {
- final Locale? locale;
final DateTime firstDate;
final DateTime lastDate;
final FhirDateTime? initialDateTime;
@@ -31,7 +30,6 @@ class FhirDateTimePicker extends StatefulWidget {
this.timePickerEntryMode = TimePickerEntryMode.dial,
this.decoration,
this.onChanged,
- this.locale,
this.focusNode,
this.enabled = true,
Key? key,
@@ -76,8 +74,8 @@ class _FhirDateTimePickerState extends State {
if (value == null || dateTime == null) return '';
return (widget.pickerType == Time)
- ? DateFormat.jm(locale.toString()).format(dateTime)
- : value.format(locale, withTimeZone: widget.pickerType == FhirDateTime);
+ ? DateFormat.jm(locale.toString()).format(dateTime)
+ : value.format(locale, withTimeZone: widget.pickerType == FhirDateTime);
}
Future _showPicker(Locale locale) async {
@@ -113,13 +111,15 @@ class _FhirDateTimePickerState extends State {
// Get new BuildContext with overridden locale
builder: (context) {
// Get time of day format of current locale
- final timeOfDayFormat = MaterialLocalizations.of(context).timeOfDayFormat();
+ final timeOfDayFormat =
+ MaterialLocalizations.of(context).timeOfDayFormat();
return MediaQuery(
data: MediaQuery.of(context).copyWith(
// Workaround for time picker validation bug in `input` mode with locales specifying a 24h TimeOfDayFormat.
// - https://github.com/sujrd/faiadashu/pull/32#issuecomment-1678639964
// - https://github.com/flutter/flutter/issues/85527
- alwaysUse24HourFormat: timeOfDay24hFormats.contains(timeOfDayFormat),
+ alwaysUse24HourFormat:
+ timeOfDay24hFormats.contains(timeOfDayFormat),
),
child: child!,
);
@@ -157,7 +157,7 @@ class _FhirDateTimePickerState extends State {
@override
Widget build(BuildContext context) {
- final locale = widget.locale ?? Localizations.localeOf(context);
+ final locale = Localizations.localeOf(context);
// There is no Locale in initState.
if (!_fieldInitialized) {
diff --git a/lib/fhir_types/src/date_time_text.dart b/lib/fhir_types/src/date_time_text.dart
index 703c471a..419cb8e8 100644
--- a/lib/fhir_types/src/date_time_text.dart
+++ b/lib/fhir_types/src/date_time_text.dart
@@ -8,20 +8,17 @@ class FhirDateTimeText extends StatelessWidget {
final FhirDateTime? dateTime;
final TextStyle? style;
final String defaultText;
- final Locale? locale;
const FhirDateTimeText(
this.dateTime, {
this.style,
this.defaultText = '',
- this.locale,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
- dateTime?.format(locale ?? Localizations.localeOf(context)) ??
- defaultText,
+ dateTime?.format(Localizations.localeOf(context)) ?? defaultText,
style: style,
);
}
diff --git a/lib/observations/src/observation_value_view.dart b/lib/observations/src/observation_value_view.dart
index e634b783..c40679f6 100644
--- a/lib/observations/src/observation_value_view.dart
+++ b/lib/observations/src/observation_value_view.dart
@@ -10,7 +10,6 @@ import 'package:intl/intl.dart';
/// or components of valueQuantity.
class ObservationValueView extends StatelessWidget {
final Observation _observation;
- final Locale? locale;
final TextStyle? valueStyle;
final TextStyle? unitStyle;
final String componentSeparator;
@@ -20,7 +19,6 @@ class ObservationValueView extends StatelessWidget {
const ObservationValueView(
this._observation, {
super.key,
- this.locale,
this.valueStyle,
this.unitStyle,
this.componentSeparator = ' | ',
@@ -33,7 +31,7 @@ class ObservationValueView extends StatelessWidget {
final Widget valueWidget;
final decimalFormat = NumberFormat.decimalPattern(
- (locale ?? Localizations.localeOf(context)).toString(),
+ Localizations.localeOf(context).toString(),
);
if (_observation.valueQuantity != null) {
diff --git a/lib/observations/src/observation_view.dart b/lib/observations/src/observation_view.dart
index 2b544493..8758a2e0 100644
--- a/lib/observations/src/observation_view.dart
+++ b/lib/observations/src/observation_view.dart
@@ -11,12 +11,10 @@ class ObservationView extends StatelessWidget {
final Widget _valueWidget;
final Widget _codeWidget;
final Widget _dateTimeWidget;
- final Locale? locale;
ObservationView(
Observation observation, {
super.key,
- this.locale,
TextStyle? valueStyle,
TextStyle? unitStyle,
TextStyle? codeStyle,
@@ -28,7 +26,6 @@ class ObservationView extends StatelessWidget {
observation,
valueStyle: valueStyle,
unitStyle: unitStyle,
- locale: locale,
componentSeparator: componentSeparator,
unknownUnitText: unknownUnitText,
unknownValueText: unknownValueText,
@@ -37,7 +34,6 @@ class ObservationView extends StatelessWidget {
CodeableConceptText(observation.code, style: codeStyle, key: key),
_dateTimeWidget = FhirDateTimeText(
observation.effectiveDateTime,
- locale: locale,
style: dateTimeStyle,
);
diff --git a/lib/questionnaires/model/aggregation/src/aggregator.dart b/lib/questionnaires/model/aggregation/src/aggregator.dart
index ac756372..d62cf3c1 100644
--- a/lib/questionnaires/model/aggregation/src/aggregator.dart
+++ b/lib/questionnaires/model/aggregation/src/aggregator.dart
@@ -1,3 +1,4 @@
+import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
import 'package:flutter/material.dart';
@@ -8,11 +9,16 @@ import 'package:flutter/material.dart';
abstract class Aggregator extends ValueNotifier {
late final QuestionnaireResponseModel questionnaireResponseModel;
late final Locale locale;
+ final FDashLocalizations localizations;
final bool autoAggregate;
/// [autoAggregate] specifies whether it should attach listeners to the
/// questionnaire and aggregate when the questionnaire changes.
- Aggregator(T initialValue, {this.autoAggregate = true}) : super(initialValue);
+ Aggregator(
+ T initialValue, {
+ required this.localizations,
+ this.autoAggregate = true,
+ }) : super(initialValue);
// ignore: use_setters_to_change_properties
/// Initialize the aggregator.
diff --git a/lib/questionnaires/model/aggregation/src/narrative_aggregator.dart b/lib/questionnaires/model/aggregation/src/narrative_aggregator.dart
index 39019a45..62ca5b9d 100644
--- a/lib/questionnaires/model/aggregation/src/narrative_aggregator.dart
+++ b/lib/questionnaires/model/aggregation/src/narrative_aggregator.dart
@@ -18,8 +18,12 @@ class NarrativeAggregator extends Aggregator {
status: NarrativeStatus.empty,
);
- NarrativeAggregator()
- : super(NarrativeAggregator.emptyNarrative, autoAggregate: false);
+ NarrativeAggregator({required FDashLocalizations localizations})
+ : super(
+ NarrativeAggregator.emptyNarrative,
+ localizations: localizations,
+ autoAggregate: false,
+ );
@override
void init(QuestionnaireResponseModel questionnaireResponseModel) {
@@ -121,7 +125,7 @@ class NarrativeAggregator extends Aggregator {
if (invalid) {
div.write(
- '${lookupFDashLocalizations(locale).dataAbsentReasonAsTextOutput} ',
+ '${localizations.dataAbsentReasonAsTextOutput} ',
);
}
@@ -129,7 +133,7 @@ class NarrativeAggregator extends Aggregator {
div.write('***
');
} else if (dataAbsentReason == dataAbsentReasonAskedButDeclinedCode) {
div.write(
- 'X ${lookupFDashLocalizations(locale).dataAbsentReasonAskedDeclinedOutput}
',
+ 'X ${localizations.dataAbsentReasonAskedDeclinedOutput}
',
);
} else {
final filledAnswers = itemModel.answeredAnswerModels;
diff --git a/lib/questionnaires/model/aggregation/src/questionnaire_response_aggregator.dart b/lib/questionnaires/model/aggregation/src/questionnaire_response_aggregator.dart
index a5af7688..ea0c811c 100644
--- a/lib/questionnaires/model/aggregation/src/questionnaire_response_aggregator.dart
+++ b/lib/questionnaires/model/aggregation/src/questionnaire_response_aggregator.dart
@@ -1,5 +1,6 @@
import 'package:faiadashu/coding/coding.dart';
import 'package:faiadashu/fhir_types/fhir_types.dart';
+import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/logging/logging.dart';
import 'package:faiadashu/questionnaires/questionnaires.dart';
import 'package:fhir/r4.dart';
@@ -13,8 +14,12 @@ class QuestionnaireResponseAggregator
extends Aggregator {
static final Logger _logger = Logger(QuestionnaireResponseAggregator);
- QuestionnaireResponseAggregator()
- : super(QuestionnaireResponse(), autoAggregate: false);
+ QuestionnaireResponseAggregator({required FDashLocalizations localizations})
+ : super(
+ QuestionnaireResponse(),
+ localizations: localizations,
+ autoAggregate: false,
+ );
QuestionnaireResponseItem? _fromQuestionItem(
QuestionItemModel itemModel,
diff --git a/lib/questionnaires/model/aggregation/src/total_score_aggregator.dart b/lib/questionnaires/model/aggregation/src/total_score_aggregator.dart
index 313929e2..a1cec24d 100644
--- a/lib/questionnaires/model/aggregation/src/total_score_aggregator.dart
+++ b/lib/questionnaires/model/aggregation/src/total_score_aggregator.dart
@@ -1,4 +1,5 @@
import 'package:collection/collection.dart';
+import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/logging/logging.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
import 'package:fhir/r4.dart';
@@ -16,8 +17,13 @@ class TotalScoreAggregator extends Aggregator {
static final _logger = Logger(TotalScoreAggregator);
late final QuestionItemModel? totalScoreItem;
- TotalScoreAggregator({bool autoAggregate = true})
- : super(Decimal(0), autoAggregate: autoAggregate);
+ TotalScoreAggregator(
+ {required FDashLocalizations localizations, bool autoAggregate = true})
+ : super(
+ Decimal(0),
+ localizations: localizations,
+ autoAggregate: autoAggregate,
+ );
@override
void init(QuestionnaireResponseModel questionnaireResponseModel) {
diff --git a/lib/questionnaires/model/item/answer/src/answer_model.dart b/lib/questionnaires/model/item/answer/src/answer_model.dart
index 70312201..4678c53b 100644
--- a/lib/questionnaires/model/item/answer/src/answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/answer_model.dart
@@ -1,5 +1,5 @@
-import 'package:faiadashu/fhir_types/fhir_types.dart';
-import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/faiadashu.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart';
import 'package:fhir/r4.dart';
import 'package:flutter/material.dart';
@@ -74,16 +74,18 @@ abstract class AnswerModel extends ResponseNode {
/// Validates a new input value. Does not change the [value].
///
- /// Returns null when [inputValue] is valid, or a localized message when it is not.
- ///
/// This is used to validate external input from a view.
- String? validateInput(I? inputValue);
+ ///
+ /// Returns null when [inputValue] is invalid; otherwise
+ /// Returns [ValidationError].
+ ValidationError? validateInput(I? inputValue);
/// Validates a value against the constraints of the answer model.
/// Does not change the [value] of the answer model.
///
- /// Returns null when it is valid, or a localized message when it is not.
- String? validateValue(V? inputValue);
+ /// Returns null when [inputValue] is invalid; otherwise
+ /// Returns [ValidationError].
+ ValidationError? validateValue(V? inputValue);
/// Validates whether the current [value] will pass the completeness check.
///
@@ -93,29 +95,27 @@ abstract class AnswerModel extends ResponseNode {
/// Since an individual answer does not know whether it is required, this
/// is not taken into account.
///
- /// Returns null when the answer is valid, or an error text,
- /// when it is not.
- ///
- String? validate({
+ /// Returns an empty list when the answer is valid, otherwise
+ /// Returns a list of [ValidationError].
+ List validate({
bool updateErrorText = true,
bool notifyListeners = false,
}) {
- final newErrorText = validateValue(
- value,
- );
+ final validationError = validateValue(value);
- if (errorText == newErrorText) {
- return newErrorText;
+ if (_validationError == validationError && validationError != null) {
+ return [validationError];
}
if (updateErrorText) {
- errorText = newErrorText;
+ _validationError = validationError;
}
+
if (notifyListeners) {
this.notifyListeners();
}
- return newErrorText;
+ return _validationError != null ? [_validationError!] : [];
}
/// Returns whether any answer (valid or invalid) has been provided.
@@ -124,12 +124,14 @@ abstract class AnswerModel extends ResponseNode {
/// Returns whether this question is unanswered.
bool get isEmpty;
- String? errorText;
+ ValidationError? _validationError;
/// Returns an error text for display in the answer's control.
///
/// This might return an error text from the parent [QuestionItemModel].
- String? get displayErrorText => errorText ?? responseItemModel.errorText;
+ String? displayErrorText(FDashLocalizations localizations) =>
+ _validationError?.getMessage(localizations) ??
+ responseItemModel.getErrorText(localizations);
/// Returns a [QuestionnaireResponseAnswer] based on the current value.
///
diff --git a/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart b/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart
index ffeab84e..5eb32e91 100644
--- a/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart
@@ -1,8 +1,9 @@
import 'package:faiadashu/fhir_types/fhir_types.dart';
-import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/max_size_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/mime_types_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart';
import 'package:fhir/r4.dart';
-import 'package:filesize/filesize.dart';
class AttachmentAnswerModel extends AnswerModel {
final num maxSize;
@@ -10,14 +11,18 @@ class AttachmentAnswerModel extends AnswerModel {
AttachmentAnswerModel(super.responseModel)
: maxSize = responseModel.questionnaireItem.extension_
- ?.extensionOrNull('http://hl7.org/fhir/StructureDefinition/maxSize')
+ ?.extensionOrNull(
+ 'http://hl7.org/fhir/StructureDefinition/maxSize')
?.valueDecimal
- ?.value ?? 0,
+ ?.value ??
+ 0,
mimeTypes = responseModel.questionnaireItem.extension_
- ?.whereExtensionIs('http://hl7.org/fhir/StructureDefinition/mimeType')
+ ?.whereExtensionIs(
+ 'http://hl7.org/fhir/StructureDefinition/mimeType')
?.map((ext) => ext.valueCode?.value ?? '')
.where((mimeType) => mimeType != '')
- .toList() ?? [];
+ .toList() ??
+ [];
@override
RenderingString get display => (value != null)
@@ -25,25 +30,26 @@ class AttachmentAnswerModel extends AnswerModel {
: RenderingString.nullText;
@override
- String? validateInput(Attachment? inValue) {
+ ValidationError? validateInput(Attachment? inValue) {
return validateValue(inValue);
}
@override
- String? validateValue(Attachment? inputValue) {
+ ValidationError? validateValue(Attachment? inputValue) {
if (inputValue == null) return null;
if (maxSize > 0) {
final attachmentSize = inputValue.size?.value;
if (attachmentSize == null || attachmentSize > maxSize) {
- return lookupFDashLocalizations(locale).validatorMaxSize(filesize(maxSize));
+ return MaxSizeError(nodeUid, maxSize);
}
}
if (mimeTypes.isNotEmpty) {
final attachmentMimeType = inputValue.contentType?.value;
- if (attachmentMimeType == null || !mimeTypes.contains(attachmentMimeType)) {
- return lookupFDashLocalizations(locale).validatorMimeTypes(mimeTypes.join(', '));
+ if (attachmentMimeType == null ||
+ !mimeTypes.contains(attachmentMimeType)) {
+ return MimeTypesError(nodeUid, mimeTypes);
}
}
diff --git a/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart b/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart
index 076a379c..28e633a8 100644
--- a/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart
@@ -1,4 +1,5 @@
import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart';
import 'package:fhir/r4.dart';
class BooleanAnswerModel extends AnswerModel {
@@ -23,12 +24,12 @@ class BooleanAnswerModel extends AnswerModel {
);
@override
- String? validateInput(Boolean? inValue) {
+ ValidationError? validateInput(Boolean? inValue) {
return null;
}
@override
- String? validateValue(Boolean? inputValue) {
+ ValidationError? validateValue(Boolean? inputValue) {
return null;
}
diff --git a/lib/questionnaires/model/item/answer/src/coding_answer_model.dart b/lib/questionnaires/model/item/answer/src/coding_answer_model.dart
index b38e86e0..c6709511 100644
--- a/lib/questionnaires/model/item/answer/src/coding_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/coding_answer_model.dart
@@ -3,6 +3,10 @@ import 'package:faiadashu/fhir_types/fhir_types.dart';
import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/logging/logging.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/max_occurs_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/min_occurs_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/single_selection_or_open_string_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart';
import 'package:faiadashu/questionnaires/questionnaires.dart';
import 'package:fhir/r4.dart';
@@ -154,15 +158,6 @@ class CodingAnswerModel extends AnswerModel {
bool get isOptionsOrString => qi.type == QuestionnaireItemType.open_choice;
- RenderingString get openLabel => RenderingString.fromText(
- qi.extension_
- ?.extensionOrNull(
- 'http://hl7.org/fhir/uv/sdc/StructureDefinition/questionnaire-sdc-openLabel',
- )
- ?.valueString ??
- lookupFDashLocalizations(locale).fillerOpenCodingOtherLabel,
- );
-
String _nextOptionUid() => _answerOptions.length.toString();
void _addAnswerOptionFromValueSetCoding(Coding coding) {
@@ -231,6 +226,17 @@ class CodingAnswerModel extends AnswerModel {
_createAnswerOptions();
}
+ RenderingString getOpenLabel(FDashLocalizations localizations) {
+ return RenderingString.fromText(
+ _openLabel ?? localizations.fillerOpenCodingOtherLabel,
+ );
+ }
+
+ String? get _openLabel => qi.extension_
+ ?.extensionOrNull(
+ 'http://hl7.org/fhir/uv/sdc/StructureDefinition/questionnaire-sdc-openLabel')
+ ?.valueString;
+
Iterable toDisplay({bool includeMedia = true}) {
final value = this.value;
if (value == null) {
@@ -301,12 +307,12 @@ class CodingAnswerModel extends AnswerModel {
}
@override
- String? validateInput(OptionsOrString? inValue) {
+ ValidationError? validateInput(OptionsOrString? inValue) {
return validateValue(inValue);
}
@override
- String? validateValue(OptionsOrString? inValue) {
+ ValidationError? validateValue(OptionsOrString? inValue) {
if (inValue == null) {
return null;
}
@@ -318,18 +324,17 @@ class CodingAnswerModel extends AnswerModel {
if (!(questionnaireItemModel.questionnaireItem.repeats?.value ?? false)) {
if (totalCount != 1) {
- return lookupFDashLocalizations(locale)
- .validatorSingleSelectionOrSingleOpenString(openLabel.plainText);
+ return SingleSelectionOrOpenStringError(nodeUid, _openLabel);
}
}
if (totalCount < minOccurs) {
- return lookupFDashLocalizations(locale).validatorMinOccurs(minOccurs);
+ return MinOccursError(nodeUid, minOccurs);
}
final maxOccurs = this.maxOccurs;
if (maxOccurs != null && totalCount > maxOccurs) {
- return lookupFDashLocalizations(locale).validatorMaxOccurs(maxOccurs);
+ return MaxOccursError(nodeUid, maxOccurs);
}
return null;
diff --git a/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart b/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart
index e1caa725..9ba3117f 100644
--- a/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart
+++ b/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart
@@ -75,6 +75,7 @@ class CodingAnswerOptionModel {
final plainText = coding.localizedDisplay(locale);
optionText = RenderingString.fromText(
plainText,
+ locale: locale,
extensions: coding.displayElement?.extension_,
);
forDisplay = plainText;
@@ -152,9 +153,7 @@ class CodingAnswerOptionModel {
final plainText =
_createMultiColumn(coding, locale, questionnaireItemModel);
forDisplay = _createForDisplay(coding, locale, questionnaireItemModel);
- optionText = RenderingString.fromText(
- plainText,
- );
+ optionText = RenderingString.fromText(plainText);
}
} else {
// The spec only allows valueCoding, but valueString occurs in the real world
@@ -169,6 +168,7 @@ class CodingAnswerOptionModel {
forDisplay = plainText;
optionText = RenderingString.fromText(
plainText,
+ locale: locale,
extensions: xhtmlExtensions,
);
}
diff --git a/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart b/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart
index ed56b9bc..711ae212 100644
--- a/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart
@@ -1,6 +1,7 @@
import 'package:faiadashu/fhir_types/fhir_types.dart';
-import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/date_time_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart';
import 'package:fhir/r4.dart'
show
Date,
@@ -53,15 +54,16 @@ class DateTimeAnswerModel extends AnswerModel {
}
@override
- String? validateInput(FhirDateTime? inValue) {
+ ValidationError? validateInput(FhirDateTime? inValue) {
return validateValue(inValue);
}
@override
- String? validateValue(FhirDateTime? inValue) {
- return inValue == null || inValue.isValid
- ? null
- : lookupFDashLocalizations(locale).validatorDateTime;
+ ValidationError? validateValue(FhirDateTime? inValue) {
+ if (!(inValue == null || inValue.isValid)) {
+ return DateTimeError(nodeUid);
+ }
+ return null;
}
@override
@@ -81,13 +83,12 @@ class DateTimeAnswerModel extends AnswerModel {
@override
void populate(QuestionnaireResponseAnswer answer) {
// NOTE: Model should probably be populated based on QuestionnaireItemType
- value = answer.valueDateTime ?? (
- (answer.valueDate != null)
- ? FhirDateTime(answer.valueDate)
- : (answer.valueTime != null)
- // TODO: Find a better way to convert Time values to FhirDateTime
- ? FhirDateTime('1970-01-01T${answer.valueTime}')
- : null
- );
+ value = answer.valueDateTime ??
+ ((answer.valueDate != null)
+ ? FhirDateTime(answer.valueDate)
+ : (answer.valueTime != null)
+ // TODO: Find a better way to convert Time values to FhirDateTime
+ ? FhirDateTime('1970-01-01T${answer.valueTime}')
+ : null);
}
}
diff --git a/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart b/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart
index c307d8de..0b72417d 100644
--- a/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart
@@ -1,8 +1,11 @@
import 'package:faiadashu/coding/coding.dart';
import 'package:faiadashu/fhir_types/fhir_types.dart';
-import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/logging/logging.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/max_value_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/min_value_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/nan_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart';
import 'package:fhir/r4.dart';
import 'package:intl/intl.dart';
@@ -161,16 +164,21 @@ class NumericalAnswerModel extends AnswerModel {
}
qi.extension_
- ?.whereExtensionIs('http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption')
- ?.forEach((extension) {
- final coding = extension.valueCoding;
- if (coding == null) return;
- _units[keyForUnitChoice(coding)] = coding;
- });
+ ?.whereExtensionIs(
+ 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption')
+ ?.forEach((extension) {
+ final coding = extension.valueCoding;
+ if (coding == null) return;
+ _units[keyForUnitChoice(coding)] = coding;
+ });
// Using updated usage for questionnaire-unit in R5 (http://hl7.org/fhir/extensions/StructureDefinition-questionnaire-unit.html)
- final questionnaireUnit = qi.extension_?.extensionOrNull('http://hl7.org/fhir/StructureDefinition/questionnaire-unit')?.valueCoding;
- if (questionnaireUnit != null && questionnaireUnit.display != null) _units[keyForUnitChoice(questionnaireUnit)] = questionnaireUnit;
+ final questionnaireUnit = qi.extension_
+ ?.extensionOrNull(
+ 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit')
+ ?.valueCoding;
+ if (questionnaireUnit != null && questionnaireUnit.display != null)
+ _units[keyForUnitChoice(questionnaireUnit)] = questionnaireUnit;
}
@override
@@ -181,7 +189,7 @@ class NumericalAnswerModel extends AnswerModel {
: RenderingString.nullText;
@override
- String? validateInput(String? inputValue) {
+ ValidationError? validateInput(String? inputValue) {
if (inputValue == null || inputValue.isEmpty) {
return null;
}
@@ -191,8 +199,8 @@ class NumericalAnswerModel extends AnswerModel {
} catch (_) {
// Ignore FormatException, number remains nan.
}
- if (number == double.nan) {
- return lookupFDashLocalizations(locale).validatorNan;
+ if (number.isNaN) {
+ return NanError(nodeUid);
}
final quantity = _valueFromNumber(number);
@@ -201,7 +209,7 @@ class NumericalAnswerModel extends AnswerModel {
}
@override
- String? validateValue(Quantity? inputValue) {
+ ValidationError? validateValue(Quantity? inputValue) {
if (inputValue == null) {
return null;
}
@@ -213,12 +221,10 @@ class NumericalAnswerModel extends AnswerModel {
}
if (number > _maxValue) {
- return lookupFDashLocalizations(locale)
- .validatorMaxValue(Decimal(_maxValue).format(locale));
+ return MaxValueError(nodeUid, Decimal(_maxValue).format(locale));
}
if (number < _minValue) {
- return lookupFDashLocalizations(locale)
- .validatorMinValue(Decimal(_minValue).format(locale));
+ return MinValueError(nodeUid, Decimal(_minValue).format(locale));
}
return null;
@@ -259,8 +265,9 @@ class NumericalAnswerModel extends AnswerModel {
/// * Updates the numerical value based on text input
/// * Keeps the unit
Quantity? copyWithTextInput(String textInput) {
- final valid = validateInput(textInput) == null;
- final dataAbsentReasonExtension = !valid
+ final valid = validateInput(textInput);
+
+ final dataAbsentReasonExtension = valid != null
? [
FhirExtension(
url: dataAbsentReasonExtensionUrl,
diff --git a/lib/questionnaires/model/item/answer/src/string_answer_model.dart b/lib/questionnaires/model/item/answer/src/string_answer_model.dart
index d8b11ab0..d0b48dfd 100644
--- a/lib/questionnaires/model/item/answer/src/string_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/string_answer_model.dart
@@ -1,7 +1,11 @@
import 'package:faiadashu/coding/coding.dart';
import 'package:faiadashu/fhir_types/fhir_types.dart';
-import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/entry_format_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/min_length_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/regex_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/url_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart';
import 'package:fhir/r4.dart';
enum StringAnswerKeyboard { plain, email, phone, number, multiline, url }
@@ -61,38 +65,37 @@ class StringAnswerModel extends AnswerModel {
: RenderingString.nullText;
@override
- String? validateInput(String? inValue) {
+ ValidationError? validateInput(String? inValue) {
final checkValue = inValue?.trim();
return validateValue(checkValue);
}
@override
- String? validateValue(String? inputValue) {
+ ValidationError? validateValue(String? inputValue) {
if (inputValue == null || inputValue.isEmpty) {
return null;
}
if (inputValue.length < minLength) {
- return lookupFDashLocalizations(locale).validatorMinLength(minLength);
+ return MinLengthError(nodeUid, minLength);
}
if (maxLength != null && inputValue.length > maxLength!) {
- return lookupFDashLocalizations(locale).validatorMaxLength(maxLength!);
+ return MinLengthError(nodeUid, maxLength!);
}
if (qi.type == QuestionnaireItemType.url) {
if (!_urlRegExp.hasMatch(inputValue)) {
- return lookupFDashLocalizations(locale).validatorUrl;
+ return UrlError(nodeUid);
}
}
if (regExp != null) {
if (!regExp!.hasMatch(inputValue)) {
return (entryFormat != null)
- ? lookupFDashLocalizations(locale)
- .validatorEntryFormat(entryFormat!)
- : lookupFDashLocalizations(locale).validatorRegExp;
+ ? EntryFormatError(nodeUid, entryFormat!)
+ : RegexError(nodeUid);
}
}
@@ -106,6 +109,7 @@ class StringAnswerModel extends AnswerModel {
final value = this.value?.trim();
final valid = validateInput(value) == null;
+
final dataAbsentReasonExtension = !valid
? [
FhirExtension(
diff --git a/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart b/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart
index f16712ef..2b5a0c08 100644
--- a/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart
@@ -1,4 +1,5 @@
import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart';
import 'package:fhir/r4.dart';
/// A pseudo-model for a questionnaire item of an unsupported type.
@@ -16,12 +17,12 @@ class UnsupportedAnswerModel extends AnswerModel
';
final prefixText = fillerItem.prefix;
+ final title = text.xhtmlText;
final htmlTitleText = (prefixText != null)
- ? '$openStyleTag${prefixText.xhtmlText} ${text.xhtmlText}$requiredTag$closeStyleTag'
- : '$openStyleTag${text.xhtmlText}$requiredTag$closeStyleTag';
+ ? '$openStyleTag${prefixText.xhtmlText} ${title}$requiredTag$closeStyleTag'
+ : '$openStyleTag${title}$requiredTag$closeStyleTag';
return QuestionnaireItemFillerTitle._(
htmlTitleText: htmlTitleText,
diff --git a/lib/questionnaires/view/src/questionnaire_complete_button.dart b/lib/questionnaires/view/src/questionnaire_complete_button.dart
index 3c3f0366..665106b2 100644
--- a/lib/questionnaires/view/src/questionnaire_complete_button.dart
+++ b/lib/questionnaires/view/src/questionnaire_complete_button.dart
@@ -31,10 +31,16 @@ class _QuestionnaireCompleteButtonState
final currentResponseStatus = qrm.responseStatus;
if (currentResponseStatus != QuestionnaireResponseStatus.completed) {
- final incompleteItems = qrm.validate(notifyListeners: true);
- qrm.invalidityNotifier.value = incompleteItems;
+ final validationErrors = qrm.validate(
+ notifyListeners: true,
+ );
+ qrm.invalidityNotifier.value = {
+ for (var validation in validationErrors)
+ validation.nodeUid:
+ validation.getMessage(FDashLocalizations.of(context)) ?? ""
+ };
- if (incompleteItems != null) {
+ if (validationErrors.isNotEmpty) {
return;
}
}
diff --git a/lib/questionnaires/view/src/questionnaire_filler.dart b/lib/questionnaires/view/src/questionnaire_filler.dart
index babc906d..1e5f3b33 100644
--- a/lib/questionnaires/view/src/questionnaire_filler.dart
+++ b/lib/questionnaires/view/src/questionnaire_filler.dart
@@ -1,3 +1,4 @@
+import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/logging/logging.dart';
import 'package:faiadashu/questionnaires/questionnaires.dart';
import 'package:faiadashu/resource_provider/resource_provider.dart';
@@ -13,7 +14,6 @@ import 'package:flutter/material.dart';
/// see: [QuestionnaireScrollerPage]
/// see: [QuestionnaireStepperPage]
class QuestionnaireResponseFiller extends StatefulWidget {
- final Locale locale;
final WidgetBuilder builder;
final List>? aggregators;
final void Function(BuildContext context, Uri url)? onLinkTap;
@@ -24,19 +24,19 @@ class QuestionnaireResponseFiller extends StatefulWidget {
final FhirResourceProvider fhirResourceProvider;
final LaunchContext launchContext;
- Future
- _createQuestionnaireResponseModel() async =>
- QuestionnaireResponseModel.fromFhirResourceBundle(
- locale: locale,
- aggregators: aggregators,
- fhirResourceProvider: fhirResourceProvider,
- launchContext: launchContext,
- questionnaireModelDefaults: questionnaireModelDefaults,
- );
+ Future _createQuestionnaireResponseModel({
+ required BuildContext context,
+ }) async =>
+ QuestionnaireResponseModel.fromFhirResourceBundle(
+ locale: Localizations.localeOf(context),
+ aggregators: aggregators,
+ fhirResourceProvider: fhirResourceProvider,
+ launchContext: launchContext,
+ questionnaireModelDefaults: questionnaireModelDefaults,
+ localizations: FDashLocalizations.of(context));
const QuestionnaireResponseFiller({
Key? key,
- required this.locale,
required this.builder,
required this.fhirResourceProvider,
required this.launchContext,
@@ -65,18 +65,11 @@ class _QuestionnaireResponseFillerState
extends State {
static final _logger = Logger(_QuestionnaireResponseFillerState);
- late final Future builderFuture;
QuestionnaireResponseModel? _questionnaireResponseModel;
VoidCallback? _handleQuestionnaireResponseModelChangeListenerFunction;
// ignore: use_late_for_private_fields_and_variables
QuestionnaireFillerData? _questionnaireFillerData;
- @override
- void initState() {
- super.initState();
- builderFuture = widget._createQuestionnaireResponseModel();
- }
-
@override
void didChangeDependencies() {
super.didChangeDependencies();
@@ -110,7 +103,6 @@ class _QuestionnaireResponseFillerState
() {
_questionnaireFillerData = QuestionnaireFillerData._(
_questionnaireResponseModel!,
- locale: widget.locale,
builder: widget.builder,
onLinkTap: widget.onLinkTap,
onDataAvailable: widget.onDataAvailable,
@@ -134,7 +126,7 @@ class _QuestionnaireResponseFillerState
_logger.trace('Enter build()');
return FutureBuilder(
- future: builderFuture,
+ future: widget._createQuestionnaireResponseModel(context: context),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.active:
@@ -152,6 +144,13 @@ class _QuestionnaireResponseFillerState
return QuestionnaireLoadingIndicator(snapshot);
}
+ // If the locale of `_questionnaireResponseModel` doesn't match the locale of `snapshot.data`,
+ // reset `_handleQuestionnaireResponseModelChangeListenerFunction` to null.
+ // This ensures that `_questionnaireFillerData` will be re-initialized
+ // using the most recent questionnaire response model that has the updated locale
+ if (_questionnaireResponseModel?.locale != snapshot.data?.locale) {
+ _handleQuestionnaireResponseModelChangeListenerFunction = null;
+ }
if (snapshot.hasData) {
_logger.debug('FutureBuilder hasData');
_questionnaireResponseModel = snapshot.data;
@@ -168,7 +167,6 @@ class _QuestionnaireResponseFillerState
_questionnaireFillerData = QuestionnaireFillerData._(
_questionnaireResponseModel!,
- locale: widget.locale,
builder: widget.builder,
onLinkTap: widget.onLinkTap,
onDataAvailable: widget.onDataAvailable,
@@ -191,7 +189,6 @@ class _QuestionnaireResponseFillerState
class QuestionnaireFillerData extends InheritedWidget {
static final _logger = Logger(QuestionnaireFillerData);
- final Locale locale;
final QuestionnaireResponseModel questionnaireResponseModel;
// TODO: Should this copy exist, or just refer to the qrm as the source of truth?
final List fillerItemModels;
@@ -206,7 +203,6 @@ class QuestionnaireFillerData extends InheritedWidget {
QuestionnaireFillerData._(
this.questionnaireResponseModel, {
Key? key,
- required this.locale,
this.onDataAvailable,
this.onLinkTap,
required this.questionnaireTheme,
@@ -260,8 +256,9 @@ class QuestionnaireFillerData extends InheritedWidget {
/// see: https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR
QuestionnaireItemFiller itemFillerAt(int index) {
_logger.trace('itemFillerAt $index');
+ final item = _itemFillers[index];
- if (_itemFillers[index] == null) {
+ if (item == null) {
_logger.debug('itemFillerAt $index will be created.');
_itemFillers[index] = questionnaireTheme.createQuestionnaireItemFiller(
this,
@@ -284,11 +281,13 @@ class QuestionnaireFillerData extends InheritedWidget {
/// that is currently visible.
int indexOfVisibleItemAt(int visibleIndex, {bool rootsOnly = false}) {
final visibleItems = fillerItemModels
- .where((item) => item.displayVisibility != DisplayVisibility.hidden)
- .where((item) => !rootsOnly || item.parentNode == null)
- .toList(growable: false);
+ .where((item) => item.displayVisibility != DisplayVisibility.hidden)
+ .where((item) => !rootsOnly || item.parentNode == null)
+ .toList(growable: false);
- return visibleItems.length > visibleIndex ? fillerItemModels.indexOf(visibleItems[visibleIndex]) : -1;
+ return visibleItems.length > visibleIndex
+ ? fillerItemModels.indexOf(visibleItems[visibleIndex])
+ : -1;
}
/// Returns the [QuestionnaireItemFiller] of the [visibleIndex]-th item that is currently
@@ -306,8 +305,8 @@ class QuestionnaireFillerData extends InheritedWidget {
if (rootIndex < 0) return [-1, -1];
final descendantsCount = fillerItemModels
- .where((item) => item.rootNode == fillerItemModels[rootIndex])
- .length;
+ .where((item) => item.rootNode == fillerItemModels[rootIndex])
+ .length;
return [rootIndex, rootIndex + descendantsCount + 1];
}
diff --git a/lib/questionnaires/view/src/questionnaire_page_scaffold.dart b/lib/questionnaires/view/src/questionnaire_page_scaffold.dart
index e392c163..3cd4ec87 100644
--- a/lib/questionnaires/view/src/questionnaire_page_scaffold.dart
+++ b/lib/questionnaires/view/src/questionnaire_page_scaffold.dart
@@ -33,11 +33,10 @@ class DefaultQuestionnairePageScaffoldBuilder
@override
Widget build(
BuildContext context, {
- Locale? locale,
required void Function(void Function()) setStateCallback,
required Widget child,
}) {
- final theLocale = locale ?? Localizations.localeOf(context);
+ final theLocale = Localizations.localeOf(context);
final questionnaireFiller = QuestionnaireResponseFiller.of(context);
final questionnaire = questionnaireFiller
diff --git a/lib/questionnaires/view/src/questionnaire_scroller.dart b/lib/questionnaires/view/src/questionnaire_scroller.dart
index 7e94a0a0..3b9d7698 100644
--- a/lib/questionnaires/view/src/questionnaire_scroller.dart
+++ b/lib/questionnaires/view/src/questionnaire_scroller.dart
@@ -26,7 +26,6 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
/// See: [QuestionnaireScrollerPage] for a [QuestionnaireScroller] which already
/// wraps the list in a ready-made [Scaffold], incl. some commonly used buttons.
class QuestionnaireScroller extends StatefulWidget {
- final Locale? locale;
final FhirResourceProvider fhirResourceProvider;
final LaunchContext launchContext;
final List>? aggregators;
@@ -38,7 +37,6 @@ class QuestionnaireScroller extends StatefulWidget {
onQuestionnaireResponseChanged;
const QuestionnaireScroller({
- this.locale,
required this.scaffoldBuilder,
required this.fhirResourceProvider,
required this.launchContext,
@@ -153,12 +151,9 @@ class _QuestionnaireScrollerState extends State {
@override
Widget build(BuildContext context) {
- final locale = widget.locale ?? Localizations.localeOf(context);
-
return QuestionnaireResponseFiller(
fhirResourceProvider: widget.fhirResourceProvider,
launchContext: widget.launchContext,
- locale: locale,
questionnaireModelDefaults: widget.questionnaireModelDefaults,
builder: (BuildContext context) {
_belowFillerContext = context;
@@ -170,50 +165,46 @@ class _QuestionnaireScrollerState extends State {
'Scroll position: ${_itemPositionsListener.itemPositions.value}',
);
- return Localizations.override(
- context: context,
- locale: locale,
- child: widget.scaffoldBuilder.build(
- context,
- setStateCallback: (fn) {
- setState(fn);
- },
- child: LayoutBuilder(
- builder: (context, constraints) {
- const edgeInsets = 8.0;
- const twice = 2;
-
- return ScrollablePositionedList.builder(
- itemScrollController: _listScrollController,
- itemPositionsListener: _itemPositionsListener,
- itemCount: totalLength,
- padding: const EdgeInsets.all(edgeInsets),
- minCacheExtent: 200, // Allow tabbing to prev/next items
- itemBuilder: (BuildContext context, int i) {
- return Row(
- children: [
- Container(
- constraints: BoxConstraints(
- maxWidth: QuestionnaireTheme.of(context)
- .maxItemWidth
- .clamp(
- constraints.minWidth,
- constraints.maxWidth - twice * edgeInsets,
- ),
- ),
- child: QuestionnaireTheme.of(context).scrollerItemBuilder(
- context,
- QuestionnaireResponseFiller.of(context),
- i,
- ),
+ return widget.scaffoldBuilder.build(
+ context,
+ setStateCallback: (fn) {
+ setState(fn);
+ },
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ const edgeInsets = 8.0;
+ const twice = 2;
+
+ return ScrollablePositionedList.builder(
+ itemScrollController: _listScrollController,
+ itemPositionsListener: _itemPositionsListener,
+ itemCount: totalLength,
+ padding: const EdgeInsets.all(edgeInsets),
+ minCacheExtent: 200, // Allow tabbing to prev/next items
+ itemBuilder: (BuildContext context, int i) {
+ return Row(
+ children: [
+ Container(
+ constraints: BoxConstraints(
+ maxWidth:
+ QuestionnaireTheme.of(context).maxItemWidth.clamp(
+ constraints.minWidth,
+ constraints.maxWidth - twice * edgeInsets,
+ ),
),
- const Spacer(),
- ],
- );
- },
- );
- },
- ),
+ child:
+ QuestionnaireTheme.of(context).scrollerItemBuilder(
+ context,
+ QuestionnaireResponseFiller.of(context),
+ i,
+ ),
+ ),
+ const Spacer(),
+ ],
+ );
+ },
+ );
+ },
),
);
},
diff --git a/lib/questionnaires/view/src/questionnaire_scroller_page.dart b/lib/questionnaires/view/src/questionnaire_scroller_page.dart
index c3cd388c..4bbc93ef 100644
--- a/lib/questionnaires/view/src/questionnaire_scroller_page.dart
+++ b/lib/questionnaires/view/src/questionnaire_scroller_page.dart
@@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
///
/// Fills up the entire page, provides default navigation, help button.
class QuestionnaireScrollerPage extends StatelessWidget {
- final Locale? locale;
final FhirResourceProvider fhirResourceProvider;
final LaunchContext launchContext;
final Widget? floatingActionButton;
@@ -16,7 +15,6 @@ class QuestionnaireScrollerPage extends StatelessWidget {
final QuestionnaireModelDefaults questionnaireModelDefaults;
const QuestionnaireScrollerPage({
- this.locale,
required this.fhirResourceProvider,
required this.launchContext,
this.floatingActionButton,
@@ -30,7 +28,6 @@ class QuestionnaireScrollerPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QuestionnaireScroller(
- locale: locale,
scaffoldBuilder: DefaultQuestionnairePageScaffoldBuilder(
// Progress can only be shown instead of a FAB
floatingActionButton: floatingActionButton ??
diff --git a/lib/questionnaires/view/src/questionnaire_stepper.dart b/lib/questionnaires/view/src/questionnaire_stepper.dart
index 16ebacbf..c2ec8fd1 100644
--- a/lib/questionnaires/view/src/questionnaire_stepper.dart
+++ b/lib/questionnaires/view/src/questionnaire_stepper.dart
@@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
/// Fill a questionnaire through a wizard-style series of individual questions.
class QuestionnaireStepper extends StatefulWidget {
- final Locale? locale;
final FhirResourceProvider fhirResourceProvider;
final LaunchContext launchContext;
final QuestionnairePageScaffoldBuilder scaffoldBuilder;
@@ -23,7 +22,6 @@ class QuestionnaireStepper extends StatefulWidget {
final void Function(FillerItemModel?)? onVisibleItemUpdated;
const QuestionnaireStepper({
- this.locale,
required this.scaffoldBuilder,
required this.fhirResourceProvider,
required this.launchContext,
@@ -59,7 +57,6 @@ class QuestionnaireStepperState extends State {
@override
Widget build(BuildContext context) {
return QuestionnaireResponseFiller(
- locale: widget.locale ?? Localizations.localeOf(context),
fhirResourceProvider: widget.fhirResourceProvider,
launchContext: widget.launchContext,
questionnaireModelDefaults: widget.questionnaireModelDefaults,
@@ -95,7 +92,8 @@ class QuestionnaireStepperState extends State {
Decimal value,
Widget? child,
) {
- final scoreString = value.value!.round().toString();
+ final scoreString =
+ value.value!.round().toString();
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
diff --git a/lib/questionnaires/view/src/questionnaire_stepper_page.dart b/lib/questionnaires/view/src/questionnaire_stepper_page.dart
index 2107bb7e..4b64ead0 100644
--- a/lib/questionnaires/view/src/questionnaire_stepper_page.dart
+++ b/lib/questionnaires/view/src/questionnaire_stepper_page.dart
@@ -8,14 +8,12 @@ import 'package:flutter/material.dart';
/// see [QuestionnaireScrollerPage]
class QuestionnaireStepperPage extends QuestionnaireStepper {
const QuestionnaireStepperPage({
- Locale? locale,
required FhirResourceProvider fhirResourceProvider,
required LaunchContext launchContext,
QuestionnaireModelDefaults questionnaireModelDefaults =
const QuestionnaireModelDefaults(),
Key? key,
}) : super(
- locale: locale,
scaffoldBuilder: const DefaultQuestionnairePageScaffoldBuilder(),
fhirResourceProvider: fhirResourceProvider,
launchContext: launchContext,
diff --git a/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart b/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart
index 26265588..aa9274a8 100644
--- a/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart
+++ b/lib/questionnaires/view/src/questionnaire_stepper_page_view.dart
@@ -30,7 +30,8 @@ class QuestionnaireStepperPageView extends StatefulWidget {
_QuestionnaireStepperPageViewState();
}
-class _QuestionnaireStepperPageViewState extends State {
+class _QuestionnaireStepperPageViewState
+ extends State {
PageController _pageController = PageController();
bool _hasRequestsRunning = false;
QuestionnaireItemFiller? _currentQuestionnaireItemFiller;
@@ -74,8 +75,10 @@ class _QuestionnaireStepperPageViewState extends State