Form Runtime Engine & Semantics
The MQUAD Form Engine is responsible for rendering, evaluating, and validating complex questionnaires on-device according to the ODK XForms / XLSForm specification.
1. Schema Pipeline & AST Representation
When a survey schema is pulled from survey-service, it is stored in the local Drift database as a version-pinned structure (SurveySchemaEntity).
Diagram source
graph LR
JSON[Survey Version JSON] --> Parser[Schema Parser]
Parser --> AST[QuestionSchema Hierarchy]
AST --> Scope[Repeat & Group Scoping]
Scope --> Engine[FormEngineNotifier]
Engine --> Dispatch[QuestionWidgetFactory]
Dispatch --> Widgets[Input Widgets]Core Schema Models:
QuestionSchema: Immutable definition of a survey item (type, name, label, hint, appearance, required, relevant expression, constraint expression, calculation formula, media URLs).ChoiceSchema: Allowed option forselect_oneorselect_multiplequestions, including name, label, and localized translations.FormState: Current runtime state holding user answers, calculation values, navigation history, bookmarks, and validation errors.
2. Dynamic Expression Evaluator
The form engine includes an in-memory expression evaluator supporting XPath and XLSForm expression syntax:
Supported Expression Categories:
- Mathematical Operations:
+,-,*,div,mod,round(),int(),number(). - Logical Operators:
and,or,not(),=,!=,<,<=,>,>=. - String Functions:
string-length(),substr(),concat(),contains(),starts-with(),regex(). - Survey-Specific Functions:
selected(${question}, 'choice_code'): Evaluates selection membership.count-selected(${question}): Returns the number of chosen options.today(),now(): Evaluates localized dates and timestamps.pulldata('dataset', 'col', 'key_col', ${key}): Evaluates external CSV/XML lookup data.indexed-repeat(${child_question}, ${repeat_group}, index): Accesses child answers within specific repeat iterations.
3. SafeHtml Sanitized Renderer & Typography
To allow questionnaire designers to format text with bolding, italics, or instructions without risking XSS or layout corruption, MQUAD uses an internal SafeHtml widget.
Typography & Normal Font Weight Standard
By default, all question labels rendered through QuestionWidgetFactory.buildLabel enforce FontWeight.normal (w400) as their base text style. This ensures that any embedded <b> or <strong> tags clearly pop out with high-contrast FontWeight.bold (w700) rendering.
// QuestionWidgetFactory.dart
static Widget buildLabel(QuestionSchema q, String? lang, {TextStyle? style}) {
final effectiveStyle = (style ?? const TextStyle()).copyWith(
fontWeight: FontWeight.normal,
);
return SafeHtml(
resolveLabel(q, lang),
style: effectiveStyle,
);
}Whitelisted Inline Formatting:
<b>,<strong>→FontWeight.bold<i>,<em>→FontStyle.italic<u>→TextDecoration.underline<strike>→TextDecoration.lineThrough<sup>,<sub>→ Superscript and subscript typography<code>,<pre>→ Monospace font family<span style="...">→ Inline CSS color, font-size, and background parsing
4. Question Dispatch & Widget Factory
QuestionWidgetFactory.build() dynamically routes each question to its dedicated presentation widget based on QuestionType:
switch (question.type) {
case QuestionType.text:
return TextInputWidget(...);
case QuestionType.integer:
case QuestionType.decimal:
return NumericInputWidget(...);
case QuestionType.selectOne:
return SelectOneInputWidget(...);
case QuestionType.selectMultiple:
return SelectMultipleInputWidget(...);
case QuestionType.date:
case QuestionType.time:
return DateTimeInputWidget(...);
case QuestionType.geopoint:
case QuestionType.geotrace:
case QuestionType.geoshape:
return LocationInputWidget(...);
case QuestionType.image:
case QuestionType.audio:
case QuestionType.video:
return MediaInputWidget(...);
case QuestionType.beginRepeat:
return RepeatWidget(...);
case QuestionType.beginGroup:
return GroupWidget(...);
case QuestionType.note:
return NoteWidget(...);
case QuestionType.signature:
return SignatureInputWidget(...);
case QuestionType.barcode:
return BarcodeInputWidget(...);
case QuestionType.acknowledge:
return AcknowledgeWidget(...);
default:
return UnsupportedQuestionWidget(question: question);
}5. Repeating Groups & Scoping
Repeat groups (begin_repeat / end_repeat) create isolated array scopes:
- Field Keys: Stored in the answer map with indexed notation:
members[0].age,members[1].age. - Skip Logic in Repeats: Relevance rules within repeat iterations (e.g. asking pregnancy status only for female household members) are evaluated independently per iteration index.
- Dynamic Repeat Counts: Supported via the
jr:countattribute, automatically inflating or truncating iterations when the controlling question changes.
6. Pre-Submit Validation & Correction Enforcement
Before any interview can be finalized:
- Relevance Filtering: Irrelevant questions are pruned so stale answers to skipped questions are never submitted.
- Constraint Verification: All constraints are evaluated against final values.
- Required Field Verification: Required questions that are visible must have non-empty answers.
- Correction Lock (G-AC4 Rule): When an enumerator reopens a rejected response for correction:
- Only the specific questions flagged by the supervisor in
review_flagsare marked as editable. - All other questions render as read-only notes, preventing accidental edits to previously approved data.
- Only the specific questions flagged by the supervisor in
