Form Runtime Engine & Semantics

Technical architecture of the MQUAD mobile form engine, XLSForm expression evaluation, SafeHtml rendering, and validation.

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; its source is available below
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 for select_one or select_multiple questions, 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:

  1. Mathematical Operations: +, -, *, div, mod, round(), int(), number().
  2. Logical Operators: and, or, not(), =, !=, <, <=, >, >=.
  3. String Functions: string-length(), substr(), concat(), contains(), starts-with(), regex().
  4. 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:count attribute, automatically inflating or truncating iterations when the controlling question changes.

6. Pre-Submit Validation & Correction Enforcement

Before any interview can be finalized:

  1. Relevance Filtering: Irrelevant questions are pruned so stale answers to skipped questions are never submitted.
  2. Constraint Verification: All constraints are evaluated against final values.
  3. Required Field Verification: Required questions that are visible must have non-empty answers.
  4. Correction Lock (G-AC4 Rule): When an enumerator reopens a rejected response for correction:
    • Only the specific questions flagged by the supervisor in review_flags are marked as editable.
    • All other questions render as read-only notes, preventing accidental edits to previously approved data.