formstack 3.0.0
formstack: ^3.0.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.
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
3.0.0 #
A correctness, extensibility and packaging release. The breaking changes are
confined to the step model; forms defined through FormStack.api().form(...)
or JSON are unaffected.
Fixed #
- Step views were never disposed. Every step view is a
StatelessWidgetholding aTextEditingController,FocusNodeandValueNotifier, and nothing in the framework releases those.FormStackViewnow disposes the form's cached views when it is removed from the tree, andFormStackFormdisposes views it evicts. A form run no longer leaks one controller set per step. Covered by a regression test. - The view cache was unbounded.
FormStackForm.maxCachedViews(default 12) now caps retained step views; the current step is never evicted. A hundred-step survey previously held every view for the lifetime of the form. - The "submitting" spinner never appeared.
isProcessingwas a plain field on aStatelessWidget, so changing it could not repaint. It is now backed by aValueNotifierand the primary button listens to it, which also means the button is genuinely disabled during an asynconBeforeFinish. - JSON parse errors were swallowed.
ParserUtils.buildFormFromJsonwas anasync voidmethod, so itsFormatExceptions escaped to the zone instead of reaching the caller —loadFromAssetreported success on a malformed file. It is synchronous again and errors propagate. ResultFormat.notEmptyrejected every string. It only recognisedList, sonotEmptyon a text field could never pass. It now acceptsString,IterableandMap. This is a widening: nothing that passed before fails.ResultFormat.notBlankaccepted whitespace." "passed anotBlankcheck because the value was never trimmed. It now trims, matching the conventional meaning of "blank".- The form-level JSON
themenever applied. Every step parsed from JSON received a fully-defaultedUIStyle, sostep.style ??= formThemenever fired and the documented"theme"key was silently ignored. Step factories now useUIStyle.maybeFrom, which returns null for an absent style. - A map location callback was an accidental set literal. The
onChangehandler was written(p0) => {formStep.result = p0}, which builds aSetcontaining the assignment's value rather than a block. It worked by accident. - The Places autocomplete built its URLs by string interpolation. The
search text went into the query unescaped, so an
&or=in what the user typed rewrote the request — appending or replacing parameters, including the API key. Both endpoints are now built withUri.httpsand encoded query parameters, and a non-200 response is handled rather than parsed. - Malformed form definitions failed obscurely. A relevant condition without
an
idorexpression, an option that is not an object, or arelevantConditionsvalue that is not a list produced aNoSuchMethodErroron a dynamic call or a condition that could never match. Each is now aFormatExceptionnaming what is wrong. - Numeric values in a JSON
themewritten as strings ("borderRadius": "12") are coerced rather than discarded. - Two
Futures returned from insidetryblocks were not awaited, so their errors bypassed the surroundingcatch. - A failed or cancelled image pick no longer clears an answer the user had
already given, and reports through
FlutterErrorrather thanprint. - The signature pad overflowed its container by 6 pixels on every build. Its wrapper capped height at 200px while the canvas, spacing and Clear button needed ~206px. Found by the new input smoke tests.
Added #
InputRegistry— register application-defined input types, or override a built-in one, without forking the library. Reachable from Dart viaInputType.custom+QuestionStep.customInputType, and from JSON by naming the registered type directly ininputType.StepRegistry— the JSON parser resolves step types through a registry instead of a hard-codedif/elsechain, so a new step type is a registration rather than a change to the parser.ValidatorRegistryand validators in JSON — a JSON-defined step can now declare"validators", unlocking the full validator library to JSON forms. Previously a JSON form could only get the default validator implied by itsinputType; anything more required dropping down to Dart. Applications can register their own named validators.ValidationResultandResultFormat.validate()— validation returns a stablecodeplus constraintparamsalongside the message, so failures can be localized or reported without string-matching.isValid/errorstill work; the new method is expressed in terms of them.FormProgress— position, total and percentage as one value object, computed in a single pass.DeviceCapabilities—BarcodeScannerandAudioRecorderports that makeInputType.barcodeandInputType.audiogenuinely functional. Both previously rendered a UI scaffold with nothing behind it, because the library declares no camera or microphone dependency. Register an adapter backed by the package of your choice and the built-in widgets use it, keeping FormStack's layout, validation and result handling. Without one,barcodefalls back to manual entry andaudiorecords a duration marker, so an app that collects only text and choices still inherits no hardware SDK.FormStackThemeScope—FormStackTheme's instance fields were inert: every call site used the static helpers with hard-coded defaults, so constructing one had no effect.maxContentWidth,contentPadding,borderRadiusandelementSpacingnow apply to the subtree, withFormStackTheme.of(context),copyWithand value equality.FormStackForm.stepAfter/stepBeforefor explicit ordered navigation.- A test suite — 155 tests, 52% line coverage, from none — covering validators, navigation and branching, JSON parsing and its failure modes, the registries, persistence, the view-disposal chain, and a smoke test that builds every built-in input type, the input registry's resolution order, the theme scope, the device-capability ports, plus a guard that the example app's own JSON assets still parse.
- A CI pipeline covering formatting, analysis with warnings fatal, tests on
Linux, macOS and Windows, the suite again on the oldest supported Flutter, an
example build, and pub.flutter-io.cn publish readiness with a
panascore threshold. ARCHITECTURE.md,SECURITY.md,CODE_OF_CONDUCT.md, issue and pull request templates, and Dependabot configuration.- An Extension Points screen in the example app
(
example/lib/extensibility_demo.dart) showing a registered custom input, a named validator resolved from a JSON spec, a device capability, and a scoped theme — with a test covering the same combination.
Changed #
- The package builds under Dart's full strictness set —
strict-casts,strict-inferenceandstrict-raw-typesare all enabled, and analysis is clean. Turning them on surfaced 251 findings, nearly all implicit downcasts fromdynamicwhere JSON crossed into typed code: they compiled, then threw at the point of use rather than at the malformed field. JsonReadertypes the parsing boundary. Every JSON factory reads through typed accessors that name the field and the step in their error, so{"count": "many"}reportsQuestionStep: "count" should be a number, got "many"instead of failing later as a cast error. Numeric strings and stringified numbers — the shapes people actually write — are coerced rather than rejected, and an unknown enum name lists the permitted values.- Callback types declare their return type.
Function(String)?and friends were bare function types that inferreddynamic; they are nowvoid Function(String)?. - Built-in inputs resolve through
InputRegistrylike everything else.QuestionStep.buildViewwas a 35-armswitch, so the library's own widgets and the extension point had different shapes.BuiltInInputsregisters each built-in withregisterIfAbsent,buildViewis a lookup, and an application override installed first is preserved. FormStepno longer extendsLinkedListEntry. A step described what to ask but was also a node in a list, which meant a step definition could belong to only one form — sharing one threw at runtime. Ordering now lives in the form. This also unblocked the Dart 3 migration:LinkedListEntryisbase, which would otherwise have forcedbaseonto every step subclass including application-defined ones.FormStackForm.stepsisList<FormStep>rather thanLinkedList<FormStep>.FormStepno longer takes a type parameter.Twas declared and never used; its only effect was to make every reference toFormStepa raw type. With it gone,strict-raw-typesis enabled and the package is free of raw types.- JSON decoding in the parser, the step factories and the Google Places models
is typed rather than reaching through
dynamic, so malformed input produces aFormatExceptioninstead of aNoSuchMethodError. getStepandgetCurrentIndexare backed by an index instead of walking the step list. The progress bar previously indexed the list twice per build.- Validator regular expressions are compiled once rather than on every keystroke.
ResultFormat.composeno longer keeps mutable state across calls.- The duplicated
inputBorder()implementation, previously copied into five input widgets, is a singleInputStyle.toInputBorderextension. - SDK floor corrected to Dart 3.10 / Flutter 3.38.2. The package declared
flutter: ">=1.17.0"while usingColor.withValues(3.27) andPopScope.onPopInvokedWithResult(3.24), so the declared floor could never have compiled. The new floor is the lowest combination CI actually builds and tests against, in themin-sdkjob — an untested floor is a guess. The binding constraint isfile_picker12, which requires Dart 3.10. - Removed the root
android/,ios/,linux/,macos/andwindows/folders. This is a pure Dart package with no native code; they wereflutter createleftovers containing only generated plugin registrants, whichflutter pub getrewrote on every machine. Platform support is declared inpubspec.yamland is unchanged at 6 of 6. - Dropped
dioandrxdart. Both existed for one file: the Places autocomplete usedDiofor two GET requests and an rxdartPublishSubjectwith.distinct().debounceTime()for its input debounce. Ahttp.Clientand aTimerdo the same work, so every consumer sheds two dependency trees.http— a far smaller package — takes their place; it was previously declared but never imported. - Upgraded
file_picker(10 → 12) andlocation(8 → 10), which were two and three major versions behind and caused resolution conflicts for applications using either package directly. Thefile_picker12 API made the web-versus-native branch in the image input unnecessary, so image picking no longer reaches fordart:io. UIStyle.maybeFromadded;UIStyle.fromis unchanged.- The public top-level
uuidvariable is gone.identifiers.dartexported a mutable top-leveluuid, so every importer of the library had that very collidable name in scope. It is package-private now. - The published API is fully documented. Every symbol reachable from
package:formstack/formstack.dartcarries a doc comment, including all 37ResultFormatfactories,FormStackForm,QuestionStep,FormStepViewand every step type. InputRegistry.buildno longer stampsResultFormat.none()onto a step that declared no validator and whose type registered no default. Doing so marked the step permanently valid and made a validator assigned later unreachable.
Tooling #
- CI actions updated to current majors:
actions/checkoutv4 → v7 (v4 pins Node 20, which the runners now force onto Node 24 with a deprecation notice on every job),codecov/codecov-actionv4 → v5, andsubosito/flutter-actionpinned tov2.23.0.
Documentation #
MIGRATION.md's v2 → v3 section was written speculatively before 3.0 existed and described a release that does not match this one — it listed OTP, HTML and map inputs as new and stated there were no breaking changes. Rewritten against what actually shipped.FAQ.mdnow covers the registries, validators in JSON, localizing validation throughValidationResult.code, the disposal contract for custom inputs, the bounded view cache, and which platform SDKs the package does and does not pull in.
Migration #
step.next/step.previous→form.stepAfter(step)/form.stepBefore(step).class MyStep extends FormStep<MyStep>→class MyStep extends FormStep.- The top-level
uuidvariable is no longer exported; usepackage:uuiddirectly if you were relying on it. - Code that typed a variable as
LinkedList<FormStep>should useList<FormStep>. - Subclasses of
FormStepneed no changes. - Review any use of
notBlank, which now rejects whitespace-only input, and ofnotEmptyon text fields, which now works.
2.5.0 - 2026-04-02 #
Added #
- 2 new example demo screens: "Data Collection (ODK)" and "Multi-Language & Offline" (total: 12 demos)
- Data Collection demo covers: RepeatStep, calculate fields, hidden fields, cascading selects, barcode, audio, geotrace, geoshape
- Multi-Language demo covers: FormStackLocale with EN/ES/FR, DisplayStep with listTile data, runtime language switching
geotraceandgeoshapeadded to README input type tables- Form-level
defaultStyleparameter for applying UIStyle to all steps at once - JSON
themekey at form level for form-wide styling from JSON
Changed #
- Example app now demonstrates all 35 input types and all 9 step types
- README examples table expanded to 12 demo screens
- Architecture file listing updated with all new files
- Input type count corrected to 36 throughout all docs
2.4.0 - 2026-04-02 #
Added #
FormStackTheme- centralized theme system with responsive sizing, dark/light mode colors, and accessibility helpers- Responsive layout: all widgets adapt to mobile (< 600px), tablet (600-1200px), and desktop (> 1200px) screens
- Dark mode support: all colors resolve from
Theme.of(context).colorSchemeinstead of hardcoded values - Semantics wrappers for accessibility on interactive elements
FormStackTheme.responsiveMaxWidth(),responsiveInputWidth(),responsivePadding(),responsiveIconSize(),responsiveButtonHeight()- Theme-aware NPS colors (
npsDetractorColor,npsPassiveColor,npsPromoterColor) - Canvas colors adapt to dark mode (
canvasStrokeColor,canvasBackgroundColor)
Changed #
- Replaced 47 hardcoded color values with theme-aware alternatives across all view files
- Replaced 45+ hardcoded BoxConstraints with responsive sizing
- Error text uses
Theme.of(context).colorScheme.errorinstead ofColors.red - Input backgrounds use
colorScheme.surfaceContainerHighestinstead of hardcoded grey - Borders use
colorScheme.outlineinstead ofColors.grey - Buttons use responsive heights based on screen size
UIStyleexpanded with 7 new properties:inputBackground,inputTextColor,titleColor,subtitleColor,iconColor,cardBackground,fontSize- all settable from JSON- Form-level
defaultStyleparameter applies to all steps without individual styling - JSON
themekey at form level applies default styling to all steps in that form
2.3.0 - 2026-04-02 #
Added #
- 2 new input types:
geotrace(trace path on map),geoshape(draw polygon on map) - Offline save & resume via
FormPersistenceinterface (enablePersistence,saveDraft,resumeDraft,deleteDraft,listDrafts) InMemoryFormPersistencebuilt-in implementation for testingFormDraftserializable model for draft stateExternalDataProviderinterface for loading options from CSV/API/databaseStaticDataProviderbuilt-in implementation withfromCsvfactoryQuestionStep.optionsProviderandoptionsSourceIdfor external data-backed choices- All new types supported in JSON parser
2.2.0 - 2026-04-02 #
Added #
- 4 new input types:
hidden(data-only, no UI),calculate(auto-computed from other results),barcode(QR/barcode scanner),audio(recording with timer) RepeatStep- dynamic repeating sections where users add/remove entries (modeled after ODKrepeat)- Cascading selects via
QuestionStep.choiceFiltercallback - filter options based on other step results (Country -> State -> City) FormStackLocaleclass for multi-language support with runtime language switching,t()andtf()translation methods, and JSON loadingQuestionStep.calculateCallbackfor auto-computing values from collected resultsQuestionStep.calculateExpressionfor declarative calculate formulas- Step view widget caching to preserve state during navigation
- All new types fully supported in JSON schema parser
Fixed #
2.1.1 - 2026-04-02 #
Fixed #
- Step view widget caching to prevent state loss during navigation (TextEditingController text, slider values, selected choices now preserved when navigating back)
- Controller and FocusNode memory leaks caused by widget recreation on every step change
- Cache cleared on form reset via
clearResult()for clean restarts - Step timestamps (
startTime,endTime) reset properly on form clear
2.1.0 - 2026-04-02 #
Added #
booleaninput type - Yes/No toggle buttonsimageChoiceinput type - select from a grid of images with labelsReviewStep- displays all collected answers for review before submissionConsentStepwithConsentSectionmodel and 8 predefined section types (overview, dataGathering, privacy, dataUse, timeCommitment, studyTasks, withdrawing, custom)- Built-in progress bar UI with step counter ("Step 3 of 10") and percentage
- Result timestamps (
startTime,endTime) recorded per step for analytics - Static image support via
titleIconImagePath(asset or network URL) - Video URL support in
InstructionStepviavideoUrl dateRangevalidator withminDate/maxDateboundsResultFormatpublic constructor for custom validator subclassingBaseStepViewandFormStepViewexported for custom input widget creationStepResultandTaskResultclasses for structured result hierarchy (modeled after ResearchKit's ORKTaskResult)- Step lifecycle callbacks:
onStepWillPresent,onStepDidComplete - API methods:
getStep(),getStepResult(),getTaskResult(),exportAsJson() - ResearchKit migration guide in README with side-by-side Swift/Dart examples
- New exports:
ReviewStep,ConsentStep,ConsentSection,ConsentSectionType,BaseStepView,FormStepView,StepResult,TaskResult
2.0.0 - 2026-04-02 #
Added #
- 8 new input types:
slider,rating,nps,consent,signature,ranking,phone,currency - 12 new validators:
min,max,range,minLength,maxLength,pattern,minSelections,maxSelections,fileSize,iban,consent,compose FormStepproperties:helperText,defaultValue,semanticLabelQuestionStepproperties:minValue,maxValue,stepValue,minSelections,maxSelections,consentText,currencySymbol,phoneCountryCode,ratingCount- New exports:
RelevantCondition,ExpressionRelevant,DynamicConditionalRelevant,NestedStep,DisplayStep,UIStyle - Full JSON schema support for all new input types and properties
- Comprehensive example app with 10 demo screens
- Dartdoc comments on all public API classes and members
Changed #
- Renamed
formKitFormtoformStackFormacross codebase - Renamed
TextFeildWidgetViewtoTextFieldWidgetView - Renamed
inputBoder()toinputBorder()across all input fields - Renamed
intputtoinputin expression evaluators - Renamed
htm_field.darttohtml_input_field.dart - Renamed
mapview_field.darttomap_input_field.dart - Replaced
GlobalKeyanti-pattern withValueNotifierinBaseStepViewandCompletionStepView - Updated
index.htmlto modernFlutterLoader.loadinitialization - Updated README with complete documentation, screenshots, and JSON schema reference
Fixed #
- Memory leak: OTP field controllers recreated on every build
- Memory leak: verification code list growing indefinitely on rebuilds
- Memory leak:
addPostFrameCallbackfiring on every rebuild in text, nested, and completion views - Performance: image memory bloat from full-resolution decoding (added
cacheWidth/cacheHeight) - Performance: dynamic key-value controller text resetting on every build
- Fixed typos in class names (
_TextesultType,_MultipleChoiceesultType) - Fixed
nextFormSatcktypo informstack_form.dart - Cleaned up dead commented-out code in
htm_field.dartandweb_view.dart - Removed unnecessary imports across all internal files
- Added
cacheExtentto allListViewwidgets for smoother scrolling
Updated #
google_maps_flutter: ^2.14.0 -> ^2.17.0google_maps_flutter_web: ^0.5.14+3 -> ^0.6.2dio: ^5.9.0 -> ^5.9.2uuid: ^4.5.2 -> ^4.5.3file_picker: ^10.3.7 -> ^10.3.10webview_flutter: ^4.13.0 -> ^4.13.1google_maps: ^8.1.1 -> ^8.2.0flutter_lints: ^2.0.0 -> ^6.0.0 (example)
1.1.1 - 2024-11-15 #
Fixed #
- Memory leak in Google Places autocomplete stream subscription
- Component disposal in nested step views
- Undefined
mountedproperty in completion step view - Auto-trigger callback firing multiple times
Changed #
- Added cache size limit (50 items) to prevent unbounded memory growth
- Optimized image memory usage by clearing file results after encoding
- Optimized controller recreation to only occur when form step result changes
- Reduced unnecessary setState calls in base step view
- Added proper GoogleMapController disposal
- Added lazy loading for background animations
Updated #
webview_flutter: ^4.9.0 -> ^4.13.0dio: ^5.8.0+1 -> ^5.9.0uuid: ^4.5.1 -> ^4.5.2lottie: ^3.3.1 -> ^3.3.2file_picker: ^8.3.7 -> ^10.3.7google_maps_flutter_web: ^0.5.12+2 -> ^0.5.14+3google_maps_flutter: ^2.12.3 -> ^2.14.0http: ^1.4.0 -> ^1.6.0
0.7.4 - 2023-10-15 #
Added #
- Dropdown button with component styles (minimal and basic)
- Dynamic key-value widget