formstack 2.5.0 copy "formstack: ^2.5.0" to clipboard
formstack: ^2.5.0 copied to clipboard

A cross-platform ResearchKit and ODK alternative for Flutter. Build dynamic forms and surveys with 35 input types, 35+ validators, repeat groups, offline save, and multi-language.

FormStack #

pub package License: MIT Flutter Platform

A cross-platform alternative to Apple ResearchKit for Flutter. Build dynamic forms, surveys, and research questionnaires from Dart objects or JSON -- on iOS, Android, Web, macOS, Windows, and Linux.

35 input types, 9 step types, 35+ validators, conditional navigation, structured consent flows, answer review, progress tracking, and full extensibility. Every ResearchKit UI pattern can be reproduced with FormStack.

Why FormStack over ResearchKit? #

ResearchKit FormStack
Platforms iOS only iOS, Android, Web, macOS, Windows, Linux
Language Swift/Objective-C Dart (Flutter)
Form source Code only Dart objects or JSON
Input types ~15 answer formats 35 input types
Validators Basic (length, regex) 35+ built-in + custom subclassing
Extensibility Subclass ORKStep Subclass FormStep, BaseStepView, ResultFormat, RelevantCondition
Results ORKTaskResult hierarchy TaskResult/StepResult with timestamps + JSON export
Consent flow ORKConsentDocument ConsentStep with sections + agreement
Review step ORKReviewStep ReviewStep with formatted answer display
Progress bar Built-in Built-in with step counter

Screenshots #

Instruction Step Name Input Email Input Multiline Text

Multiple Choice Multiple Choice Selected Single Choice Time Picker

Date Picker Smile Rating Completion Step

ResearchKit to FormStack Migration #

If you're migrating from Apple ResearchKit, here's how the concepts map:

Step Types #

ResearchKit (iOS) FormStack (Flutter) Notes
ORKInstructionStep InstructionStep Supports Lottie animations, static images, and video URLs
ORKQuestionStep QuestionStep 35 input types vs ResearchKit's ~15
ORKFormStep NestedStep Multiple fields on one screen with cross-field validation
ORKCompletionStep CompletionStep Loading/success/error Lottie animations, async callbacks
ORKConsentDocument + ORKVisualConsentStep ConsentStep Expandable sections with 8 predefined types
ORKConsentReviewStep ConsentStep (agreement checkbox) Built into ConsentStep
ORKReviewStep ReviewStep Displays all answers before submission
ORKSignatureStep QuestionStep(inputType: InputType.signature) Canvas drawing, returns base64 PNG
ORKWebViewStep DisplayStep(displayStepType: DisplayStepType.web) WebView content
ORKNavigableOrderedTask FormStack.api().form(steps: [...]) Conditional navigation via relevantConditions

Answer Formats #

ResearchKit (iOS) FormStack (Flutter) Code
ORKTextAnswerFormat InputType.text QuestionStep(inputType: InputType.text, numberOfLines: 3)
ORKNumericAnswerFormat InputType.number QuestionStep(inputType: InputType.number)
ORKScaleAnswerFormat InputType.slider QuestionStep(inputType: InputType.slider, minValue: 0, maxValue: 10)
ORKBooleanAnswerFormat InputType.boolean QuestionStep(inputType: InputType.boolean)
ORKTextChoiceAnswerFormat (single) InputType.singleChoice QuestionStep(inputType: InputType.singleChoice, options: [...])
ORKTextChoiceAnswerFormat (multi) InputType.multipleChoice QuestionStep(inputType: InputType.multipleChoice, options: [...])
ORKImageChoiceAnswerFormat InputType.imageChoice QuestionStep(inputType: InputType.imageChoice, options: [...])
ORKValuePickerAnswerFormat InputType.dropdown QuestionStep(inputType: InputType.dropdown, options: [...])
ORKDateAnswerFormat InputType.date QuestionStep(inputType: InputType.date)
ORKTimeOfDayAnswerFormat InputType.time QuestionStep(inputType: InputType.time)
ORKLocationAnswerFormat InputType.mapLocation QuestionStep(inputType: InputType.mapLocation)
ResearchKit (iOS) FormStack (Flutter) Code
ORKPredicateStepNavigationRule ExpressionRelevant ExpressionRelevant(identifier: GenericIdentifier(id: "step"), expression: "IN value")
ORKDirectStepNavigationRule ExpressionRelevant with FOR_ALL ExpressionRelevant(identifier: ..., expression: "FOR_ALL")
Custom delegate DynamicConditionalRelevant DynamicConditionalRelevant(identifier: ..., isValidCallBack: (result) => ...)

Results #

ResearchKit (iOS) FormStack (Flutter) Code
ORKTaskResult TaskResult FormStack.api().getTaskResult()
ORKStepResult StepResult Includes startTime, endTime, duration, value
result.startDate / endDate step.startTime / endTime Automatic timestamp recording
JSON serialization exportAsJson() FormStack.api().exportAsJson(formName: "myForm")

Quick Migration Example #

ResearchKit (Swift):

let step1 = ORKInstructionStep(identifier: "intro")
step1.title = "Welcome"
step1.text = "This survey takes 5 minutes"

let step2 = ORKQuestionStep(identifier: "name")
step2.title = "Your Name"
step2.answerFormat = ORKTextAnswerFormat(maximumLength: 50)

let step3 = ORKQuestionStep(identifier: "satisfaction")
step3.title = "How satisfied are you?"
step3.answerFormat = ORKScaleAnswerFormat(maximumValue: 10, minimumValue: 0, defaultValue: 5, step: 1)

let task = ORKOrderedTask(identifier: "survey", steps: [step1, step2, step3])
let taskVC = ORKTaskViewController(task: task, taskRun: nil)

FormStack (Dart) - same UI, all platforms:

FormStack.api().form(steps: [
  InstructionStep(
    id: GenericIdentifier(id: "intro"),
    title: "Welcome",
    text: "This survey takes 5 minutes",
  ),
  QuestionStep(
    id: GenericIdentifier(id: "name"),
    title: "Your Name",
    inputType: InputType.name,
    lengthLimit: 50,
  ),
  QuestionStep(
    id: GenericIdentifier(id: "satisfaction"),
    title: "How satisfied are you?",
    inputType: InputType.slider,
    minValue: 0,
    maxValue: 10,
    stepValue: 1,
    defaultValue: 5,
  ),
]);

// Render in any Flutter widget
Scaffold(body: FormStack.api().render());

Installation #

dependencies:
  formstack: ^2.5.0
flutter pub get

Quick Start #

import 'package:formstack/formstack.dart';

// Build and render a form
FormStack.api().form(steps: [
  QuestionStep(
    title: "Your Name",
    inputType: InputType.name,
    id: GenericIdentifier(id: "name"),
  ),
  QuestionStep(
    title: "Email",
    inputType: InputType.email,
    id: GenericIdentifier(id: "email"),
  ),
  CompletionStep(
    title: "Done!",
    id: GenericIdentifier(id: "done"),
    onFinish: (result) => print(result),
  ),
]);

// In your widget
Scaffold(body: FormStack.api().render());

Or load from JSON:

await FormStack.api().loadFromAsset('assets/form.json');
Scaffold(body: FormStack.api().render());

Supported Input Types #

Text Inputs #

InputType Description Keyboard Validation
email Email address Email keyboard Regex email validation
name Person name Text, auto-capitalize words Letters only, min 2 chars
password Secure password Visible password Uppercase, lowercase, digit, special char, 8+
text General text Multiline Non-empty, configurable numberOfLines
number Numeric input Number pad Digits only, supports mask formatting
phone Phone with country code Phone Country code dropdown + E.164 format
currency Money amount Decimal number Currency symbol prefix, formatted input

Date & Time #

InputType Description Result Type
date Date picker DateTime
time Time picker DateTime
dateTime Combined date + time DateTime

Choice Inputs #

InputType Description Selection Styles
singleChoice Select one option arrow, tick, toggle
multipleChoice Select multiple tick, toggle
dropdown Dropdown menu Standard dropdown
ranking Drag-to-reorder list Reorderable with rank numbers
boolean Yes/No toggle buttons Two-button selection
imageChoice Select from a grid of images Image cards with labels

Survey & Rating #

InputType Description Result Type
slider Range slider double (configurable min/max/step)
rating Star rating int (1 to N stars)
nps Net Promoter Score (0-10) int (color-coded scale)
smile Emoji satisfaction int (1-5 scale)

Media & Files #

InputType Description Result Type
file File picker with filters PlatformFile
avatar Circular image upload String (base64)
banner Rectangular image upload String (base64)
signature Draw signature on canvas String (base64 PNG)
mapLocation Google Maps picker Location coordinates
geotrace Trace a path/line on map List<Map> (lat/lng points)
geoshape Draw a polygon on map List<Map> (lat/lng points)

Special #

InputType Description Result Type
otp Multi-digit OTP entry int
consent Checkbox with agreement text bool
dynamicKeyValue Add/remove key-value pairs List<KeyValue>
htmlEditor Rich text editor String
hidden Hidden data field (no UI, auto-advances) dynamic
calculate Auto-computed from other results dynamic
barcode Barcode/QR scanner with manual fallback String
audio Audio recording with timer String

Step Types #

InstructionStep #

Welcome screens, information pages, instructions.

InstructionStep(
  id: GenericIdentifier(id: "welcome"),
  title: "Customer Survey",
  text: "This will take about 2 minutes",
  cancellable: false,
  display: Display.medium,
)

QuestionStep #

The main input step. Supports all 28 input types.

QuestionStep(
  id: GenericIdentifier(id: "email"),
  title: "Email Address",
  text: "We'll send your receipt here",
  inputType: InputType.email,
  inputStyle: InputStyle.outline,
  isOptional: false,
  hint: "you@example.com",
)

CompletionStep #

Form completion with loading/success/error animations.

CompletionStep(
  id: GenericIdentifier(id: "done"),
  title: "Submitting...",
  autoTrigger: false,
  onFinish: (result) => print("Result: $result"),
  onBeforeFinishCallback: (result) async {
    await submitToApi(result);
    return true; // false shows error animation
  },
)

NestedStep #

Multiple fields on a single screen.

NestedStep(
  id: GenericIdentifier(id: "contact"),
  title: "Contact Information",
  verticalPadding: 10,
  validationExpression: "",
  steps: [
    QuestionStep(title: "", inputType: InputType.name, label: "First Name",
        id: GenericIdentifier(id: "first"), width: 400),
    QuestionStep(title: "", inputType: InputType.name, label: "Last Name",
        id: GenericIdentifier(id: "last"), width: 400),
    QuestionStep(title: "", inputType: InputType.email, label: "Email",
        id: GenericIdentifier(id: "email"), width: 400),
  ],
)

ReviewStep #

Displays all collected answers for review before submission. Place before CompletionStep.

ReviewStep(
  id: GenericIdentifier(id: "review"),
  title: "Review Your Answers",
  text: "Verify before submitting",
  nextButtonText: "Submit",
)

ConsentStep #

Structured consent document with expandable sections, agreement checkbox, and optional signature. Modeled after Apple ResearchKit's consent flow.

ConsentStep(
  id: GenericIdentifier(id: "consent"),
  title: "Informed Consent",
  requiresSignature: true,
  agreementText: "I agree to participate",
  sections: [
    ConsentSection(
      type: ConsentSectionType.overview,
      title: "About This Study",
      summary: "Brief overview...",
      content: "Full details shown on expand...",
    ),
    ConsentSection(
      type: ConsentSectionType.privacy,
      title: "Your Privacy",
      summary: "Data is encrypted and anonymized.",
    ),
    ConsentSection(
      type: ConsentSectionType.withdrawing,
      title: "Withdrawing",
      summary: "You can stop at any time.",
    ),
  ],
)

ConsentSectionType values: overview, dataGathering, privacy, dataUse, timeCommitment, studyTasks, withdrawing, custom

RepeatStep #

Dynamic repeating sections where users add/remove entries. Modeled after ODK's repeat.

RepeatStep(
  id: GenericIdentifier(id: "members"),
  title: "Household Members",
  minRepeat: 1,
  maxRepeat: 10,
  addButtonText: "Add Member",
  steps: [
    QuestionStep(title: "", inputType: InputType.name, label: "Name",
        id: GenericIdentifier(id: "name"), width: 400),
    QuestionStep(title: "", inputType: InputType.number, label: "Age",
        id: GenericIdentifier(id: "age"), width: 400),
  ],
)

Result: List<Map<String, dynamic>> - one map per repetition.

DisplayStep #

Show web content or data lists.

DisplayStep(
  id: GenericIdentifier(id: "info"),
  url: "https://example.com/terms",
  displayStepType: DisplayStepType.web,
)

Validation #

Built-in Validators #

// Text validators
ResultFormat.email("Invalid email")
ResultFormat.name("Invalid name")
ResultFormat.password("Weak password")
ResultFormat.text("Required")
ResultFormat.number("Must be a number")
ResultFormat.phone("Invalid phone")
ResultFormat.url("Invalid URL")

// Numeric range validators
ResultFormat.min("Must be at least 18", 18)
ResultFormat.max("Cannot exceed 100", 100)
ResultFormat.range("Must be 1-10", 1, 10)
ResultFormat.age("Invalid age (0-150)")
ResultFormat.percentage("Must be 0-100")

// String length validators
ResultFormat.minLength("Too short", 3)
ResultFormat.maxLength("Too long", 50)
ResultFormat.pattern("Invalid format", r'^[A-Z]{3}\d{4}$')
ResultFormat.length("Must be exactly 6 digits", 6)

// Choice validators
ResultFormat.singleChoice("Please select one")
ResultFormat.multipleChoice("Select at least one")
ResultFormat.minSelections("Select at least 2", 2)
ResultFormat.maxSelections("Select at most 3", 3)

// Specialty validators
ResultFormat.creditCard("Invalid card number")    // Luhn algorithm
ResultFormat.ssn("Invalid SSN")                    // ###-##-####
ResultFormat.zipCode("Invalid zip")                // #####(-####)
ResultFormat.iban("Invalid IBAN")                  // ISO 13616
ResultFormat.consent("You must agree")             // Must be true
ResultFormat.dateRange("Invalid date", "dd-MM-yyyy",
    minDate: DateTime(2000), maxDate: DateTime(2030)) // Date bounds
ResultFormat.fileSize("File too large", 5242880)   // Max bytes
ResultFormat.notNull("Required")
ResultFormat.notBlank("Cannot be empty")
ResultFormat.notEmpty("List cannot be empty")

// Custom validator
ResultFormat.custom("Must start with 'hello'",
    (value) => value.startsWith('hello'))

// Compose multiple validators
ResultFormat.compose([
  ResultFormat.minLength("Too short", 3),
  ResultFormat.maxLength("Too long", 50),
  ResultFormat.pattern("Letters only", r'^[a-zA-Z\s]+$'),
])

Styling #

Input Styles #

inputStyle: InputStyle.basic      // Flat, no border
inputStyle: InputStyle.outline    // Full border
inputStyle: InputStyle.underLined // Bottom border only

Component Styles #

componentsStyle: ComponentsStyle.minimal  // Clean, minimal
componentsStyle: ComponentsStyle.basic    // Card-style with background

Display Sizes #

display: Display.small       // Compact
display: Display.normal      // Standard (default)
display: Display.medium      // Larger headings
display: Display.large       // Big text
display: Display.extraLarge  // Maximum size

Selection Types (for choices) #

selectionType: SelectionType.arrow    // Navigate arrows
selectionType: SelectionType.tick     // Checkmark
selectionType: SelectionType.toggle   // Switch toggle
selectionType: SelectionType.dropdown // Dropdown menu

Custom Theme (Dart) #

QuestionStep(
  title: "Styled",
  inputType: InputType.text,
  style: UIStyle(
    Colors.indigo,       // Button background
    Colors.white,        // Button foreground
    Colors.indigo,       // Input border color
    8.0,                 // Title bottom padding
    12.0,                // Button border radius
    inputBackground: Colors.grey.shade100,
    inputTextColor: Colors.black87,
    titleColor: Colors.indigo,
    iconColor: Colors.indigo,
    cardBackground: Colors.white,
    fontSize: 16.0,
  ),
)

Custom Theme (JSON) #

Apply a form-level theme to all steps, or style individual steps:

{
  "my_form": {
    "theme": {
      "backgroundColor": "#3F51B5",
      "foregroundColor": "#FFFFFF",
      "borderColor": "#3F51B5",
      "borderRadius": 12,
      "inputBackground": "#F5F5F5",
      "inputTextColor": "#212121",
      "titleColor": "#3F51B5",
      "subtitleColor": "#757575",
      "iconColor": "#3F51B5",
      "cardBackground": "#FFFFFF",
      "fontSize": 16
    },
    "steps": [
      {
        "type": "QuestionStep",
        "id": "name",
        "title": "Your Name",
        "inputType": "name",
        "style": {
          "backgroundColor": "#FF5722",
          "foregroundColor": "#FFFFFF",
          "borderRadius": 24
        }
      }
    ]
  }
}

The theme key applies to all steps as a default. Individual step style overrides the form theme.

Dark Mode #

FormStack automatically adapts to your app's theme. No configuration needed:

MaterialApp(
  theme: ThemeData.light(useMaterial3: true),
  darkTheme: ThemeData.dark(useMaterial3: true),
  themeMode: ThemeMode.system, // Auto dark/light
  home: Scaffold(body: FormStack.api().render()),
)

All colors resolve from Theme.of(context).colorScheme at runtime.


Cascading Selects #

Filter choices based on previous answers:

QuestionStep(
  title: "State",
  inputType: InputType.dropdown,
  options: allStates, // Full list
  choiceFilter: (options, results) =>
      options.where((o) => o.value == results["country"]).toList(),
)

Calculated Fields #

Auto-compute values from other step results:

QuestionStep(
  title: "BMI",
  inputType: InputType.calculate,
  calculateCallback: (results) {
    final weight = double.tryParse(results["weight"]?.toString() ?? "") ?? 0;
    final height = double.tryParse(results["height"]?.toString() ?? "") ?? 1;
    return (weight / (height * height)).toStringAsFixed(1);
  },
  helperText: "Calculated from height and weight",
)

Multi-Language Support #

final locale = FormStackLocale(
  defaultLocale: 'en',
  translations: {
    'en': {'name_title': 'Your Name', 'name_hint': 'Enter full name'},
    'es': {'name_title': 'Tu Nombre', 'name_hint': 'Ingrese nombre completo'},
    'fr': {'name_title': 'Votre Nom', 'name_hint': 'Entrez le nom complet'},
  },
);

QuestionStep(
  title: locale.t('name_title'),
  hint: locale.t('name_hint'),
  inputType: InputType.name,
)

// Switch language at runtime
locale.setLocale('es');

Conditional Navigation #

Route users to different steps based on their answers:

QuestionStep(
  id: GenericIdentifier(id: "role"),
  title: "Your Role",
  inputType: InputType.singleChoice,
  autoTrigger: true,
  options: [
    Options("dev", "Developer"),
    Options("designer", "Designer"),
  ],
  relevantConditions: [
    ExpressionRelevant(
      identifier: GenericIdentifier(id: "dev_questions"),
      expression: "IN dev",
    ),
    ExpressionRelevant(
      identifier: GenericIdentifier(id: "design_questions"),
      expression: "IN designer",
    ),
  ],
)

Expression syntax:

  • IN value - Result contains the value
  • NOT_IN value - Result does not contain the value
  • FOR_ALL - Always matches (converge paths)
  • = value / != value - Exact match

Cross-form navigation with formName:

ExpressionRelevant(
  identifier: GenericIdentifier(id: "step_id"),
  expression: "IN selected_option",
  formName: "another_form",  // Navigate to a different form
)

API Reference #

FormStack #

// Create/get instance (supports named instances)
FormStack.api()
FormStack.api(name: "myForm")

// Build form from Dart objects
FormStack.api().form(steps: [...], name: "formName")

// Load from JSON
await FormStack.api().loadFromAsset('assets/form.json')
await FormStack.api().loadFromAssets(['assets/a.json', 'assets/b.json'])

// Render
FormStack.api().render()
FormStack.api().render(name: "formName")

// Pre-fill data
FormStack.api().setResult({"email": "user@test.com"}, formName: "myForm")

// Set validation error on a field
FormStack.api().setError(GenericIdentifier(id: "email"), "Already taken", formName: "myForm")

// Update options dynamically
FormStack.api().setOptions([Options("a", "A")], GenericIdentifier(id: "choice"))

// Disable specific fields
FormStack.api().setDisabledUI(["field_id_1", "field_id_2"])

// Completion callback with async pre-validation
FormStack.api().addCompletionCallback(
  GenericIdentifier(id: "done"),
  formName: "myForm",
  onFinish: (result) => print(result),
  onBeforeFinishCallback: (result) async {
    final success = await api.submit(result);
    return success; // false = show error animation
  },
);

// Progress tracking
double progress = FormStack.api().getFormProgress();       // 0.0 - 1.0
int step = FormStack.api().getCurrentStepIndex();
bool done = FormStack.api().isFormCompleted();
Map stats = FormStack.api().getFormStats();
// stats: {totalSteps, completedSteps, requiredSteps, optionalSteps, progress, isCompleted}

// Back navigation control
FormStack.api().systemBackNavigation(true, () => print("Back pressed"));

// Cleanup
FormStack.clearForms();
FormStack.api().clearConfiguration();

JSON Schema #

All step types and properties are supported in JSON. Wrap forms in a named object:

{
  "my_form": {
    "backgroundAnimationFile": "assets/bg.json",
    "steps": [
      {
        "type": "InstructionStep",
        "id": "welcome",
        "title": "Welcome",
        "text": "Complete this survey",
        "cancellable": false,
        "display": "medium"
      },
      {
        "type": "QuestionStep",
        "id": "name",
        "title": "Full Name",
        "inputType": "name",
        "inputStyle": "outline",
        "isOptional": false,
        "hint": "John Doe",
        "helperText": "Enter your legal name"
      },
      {
        "type": "QuestionStep",
        "id": "satisfaction",
        "title": "How satisfied are you?",
        "inputType": "slider",
        "minValue": 0,
        "maxValue": 10,
        "stepValue": 1,
        "defaultValue": 5
      },
      {
        "type": "QuestionStep",
        "id": "rating",
        "title": "Rate our service",
        "inputType": "rating",
        "ratingCount": 5
      },
      {
        "type": "QuestionStep",
        "id": "recommend",
        "title": "Would you recommend us?",
        "inputType": "nps"
      },
      {
        "type": "QuestionStep",
        "id": "country",
        "title": "Country",
        "inputType": "dropdown",
        "componentsStyle": "basic",
        "options": [
          {"key": "US", "title": "United States"},
          {"key": "UK", "title": "United Kingdom"},
          {"key": "IN", "title": "India"}
        ]
      },
      {
        "type": "QuestionStep",
        "id": "phone",
        "title": "Phone",
        "inputType": "phone",
        "phoneCountryCode": "+1"
      },
      {
        "type": "QuestionStep",
        "id": "budget",
        "title": "Budget",
        "inputType": "currency",
        "currencySymbol": "$"
      },
      {
        "type": "QuestionStep",
        "id": "priorities",
        "title": "Rank by priority",
        "inputType": "ranking",
        "options": [
          {"key": "speed", "title": "Speed"},
          {"key": "quality", "title": "Quality"},
          {"key": "cost", "title": "Cost"}
        ]
      },
      {
        "type": "QuestionStep",
        "id": "agree",
        "title": "Agreement",
        "inputType": "consent",
        "consentText": "I agree to the terms and conditions"
      },
      {
        "type": "QuestionStep",
        "id": "sig",
        "title": "Signature",
        "inputType": "signature"
      },
      {
        "type": "CompletionStep",
        "id": "done",
        "title": "Thank you!",
        "autoTrigger": true
      }
    ]
  }
}

Supported JSON Fields #

All Steps: type, id, title, text, description, hint, label, display, isOptional, cancellable, disabled, nextButtonText, backButtonText, cancelButtonText, footerBackButton, componentsStyle, crossAxisAlignmentContent, titleIconAnimationFile, titleIconMaxWidth, width, helperText, defaultValue, semanticLabel, style

QuestionStep: inputType, inputStyle, options, selectionType, autoTrigger, numberOfLines, count, maxCount, mask, filter, maxHeight, lengthLimit, textAlign, relevantConditions, minValue, maxValue, stepValue, minSelections, maxSelections, consentText, currencySymbol, phoneCountryCode, ratingCount

CompletionStep: autoTrigger, successLottieAssetsFilePath, loadingLottieAssetsFilePath, errorLottieAssetsFilePath

NestedStep: steps, validationExpression, verticalPadding

DisplayStep: url, displayStepType, data


Examples #

The example app demonstrates all features across 12 demo screens:

Demo Features
All Input Types Core input types: text, email, name, password, number, date, time, choices, OTP, smile, file, key-value, avatar, banner
Styles & Display InputStyle, ComponentsStyle, Display sizes, UIStyle, JSON theming, dark mode
Selection Types Arrow, tick, toggle, dropdown
Validation Email, password, phone, URL, age, zip, dateRange, custom, compose
Conditional Nav ExpressionRelevant branching and path convergence
Nested Steps Multi-field screens with cross-field validation
API Features setResult, setError, callbacks, progress tracking
Survey Components Slider, rating, NPS, consent, signature, ranking, phone, currency
ResearchKit Features Boolean, image choice, consent flow, review step, progress bar, timestamps
Data Collection (ODK) RepeatStep, calculate, hidden, cascading selects, barcode, audio, geotrace, geoshape
Multi-Language & Offline FormStackLocale, runtime language switching, DisplayStep, offline save/resume
Load from JSON Multi-file JSON loading with form linking

Run the example:

cd example
flutter run

Extensibility #

FormStack is designed for extension at every layer, following Apple ResearchKit's architecture.

Custom Validators #

Subclass ResultFormat directly:

class PalindromeValidator extends ResultFormat {
  final String errorMsg;
  PalindromeValidator(this.errorMsg);

  @override
  bool isValid(dynamic input) {
    final str = (input as String?)?.toLowerCase() ?? '';
    return str == str.split('').reversed.join();
  }

  @override
  String error() => errorMsg;
}

// Use it
QuestionStep(
  inputType: InputType.text,
  resultFormat: PalindromeValidator("Must be a palindrome"),
)

Custom Input Widgets #

Extend BaseStepView to create custom inputs:

class ColorPickerWidget extends BaseStepView<QuestionStep> {
  ColorPickerWidget(super.formStackForm, super.formStep, super.text);

  Color _selected = Colors.red;

  @override
  Widget? buildWInputWidget(BuildContext context, QuestionStep formStep) {
    return StatefulBuilder(builder: (context, setState) {
      // Your custom color picker UI here
    });
  }

  @override
  bool isValid() => true;
  @override
  String validationError() => "";
  @override
  dynamic resultValue() => _selected.value;
  @override
  void requestFocus() {}
  @override
  void clearFocus() {}
}

Custom Step Types #

Subclass FormStep for entirely new step types:

class VideoStep extends FormStep {
  final String videoUrl;
  VideoStep({required this.videoUrl, super.id, super.title});

  @override
  FormStepView buildView(FormStackForm formStackForm) {
    return MyVideoStepView(formStackForm, this, text);
  }
}

Custom Navigation Rules #

Subclass RelevantCondition:

class ScoreThresholdCondition extends RelevantCondition {
  final int threshold;
  ScoreThresholdCondition({required super.identifier, required this.threshold});

  @override
  bool isValid(dynamic result) => (result as int?) != null && result >= threshold;
}

Step Lifecycle Callbacks #

Hook into step events for analytics or custom behavior:

QuestionStep(
  title: "Email",
  inputType: InputType.email,
  onStepWillPresent: (step) => analytics.trackStepView(step.id?.id),
  onStepDidComplete: (step, result) => analytics.trackStepComplete(step.id?.id, result),
)

Structured Result Export #

Get typed results with timestamps (modeled after ResearchKit's ORKTaskResult):

final taskResult = FormStack.api().getTaskResult(formName: "myForm");
print(taskResult.totalDuration);
print(taskResult.completedSteps);

// Full JSON export with step-level timestamps and metadata
final json = FormStack.api().exportAsJson(formName: "myForm");
await http.post('/api/submit', body: jsonEncode(json));

// Access individual step results
final email = FormStack.api().getStepResult("email");
final step = FormStack.api().getStep("email");
print(step?.startTime); // When user saw this step
print(step?.endTime);   // When user completed it

Architecture #

lib/
  formstack.dart              # Public API exports
  src/
    formstack.dart             # FormStack singleton API
    formstack_form.dart        # Form navigation and state
    input_types.dart           # InputType enum (35 values)
    core/
      form_step.dart           # Base FormStep class, enums
      parser.dart              # JSON parser
      ui_style.dart            # UIStyle, HexColor
      form_locale.dart         # FormStackLocale (multi-language)
    step/
      question_step.dart       # QuestionStep (all input types)
      completion_step.dart     # CompletionStep (finish with animation)
      instruction_step.dart    # InstructionStep (info/video screens)
      nested_step.dart         # NestedStep (multi-field)
      display_step.dart        # DisplayStep (web/list content)
      review_step.dart         # ReviewStep (answer review before submit)
      consent_step.dart        # ConsentStep (consent document flow)
      repeat_step.dart         # RepeatStep (dynamic repeating sections)
      pop_step.dart            # PopStep (navigation)
    result/
      result_format.dart       # 35+ validators (subclassable)
      step_result.dart         # StepResult, TaskResult hierarchy
      common_result.dart       # Options, KeyValue, DynamicData
      identifiers.dart         # GenericIdentifier, StepIdentifier
    relevant/
      relevant_condition.dart  # RelevantCondition base
      expression_relevant_condition.dart  # Expression-based routing
      dynamic_relevant_condition.dart     # Callback-based routing
    ui/views/input/
      text_input_field.dart    # Text/email/password/number
      choice_input_field.dart  # Single/multiple choice
      date_input_field.dart    # Date/time pickers
      slider_input_field.dart  # Range slider
      rating_input_field.dart  # Star rating
      nps_input_field.dart     # Net Promoter Score
      consent_input_field.dart # Checkbox consent
      signature_input_field.dart # Signature pad
      ranking_input_field.dart # Drag-to-reorder
      phone_input_field.dart   # Phone with country code
      currency_input_field.dart # Currency input
      boolean_input_field.dart  # Yes/No toggle
      image_choice_input_field.dart # Image grid selection
      otp_input_field.dart     # OTP digits
      smile_input_field.dart   # Emoji rating
      image_input_field.dart   # Avatar/banner upload
      dynamic_key_value_field.dart # Key-value pairs
      map_input_field.dart     # Google Maps
      hidden_input_field.dart   # Hidden data field
      calculate_input_field.dart # Calculated field
      barcode_input_field.dart  # Barcode/QR scanner
      audio_input_field.dart    # Audio recording
      geotrace_input_field.dart # Geotrace/geoshape map input
      html_input_field.dart    # Rich text editor

License #

MIT License. See LICENSE for details.

39
likes
150
points
604
downloads
screenshot

Documentation

API reference

Publisher

verified publishersudhi.in

Weekly Downloads

A cross-platform ResearchKit and ODK alternative for Flutter. Build dynamic forms and surveys with 35 input types, 35+ validators, repeat groups, offline save, and multi-language.

Repository (GitHub)
View/report issues
Contributing

Topics

#form #survey #ui #json #widget

License

MIT (license)

Dependencies

collection, dio, file_picker, flutter, google_maps, google_maps_flutter, google_maps_flutter_web, http, intl, location, lottie, mask_text_input_formatter, reviews_slider, rxdart, uuid, web, webview_flutter

More

Packages that depend on formstack