mk_forms 1.3.0
mk_forms: ^1.3.0 copied to clipboard
A flexible, reactive, and type-safe Flutter form management library powered by flutter_bloc. Handles minimal to complex forms with ease.
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.
1.3.0 - 2026-10-11 #
Added #
MkArrayBuilder<C>— builds from anMkArrayBlocand, by default, rebuilds only when the list of controls changes (add/insert/remove/move), not when a row's value changes. A plainBlocBuilderon an array rebuilds every row on every keystroke in any row, becauseMkArrayState.valueschanges each time. AcceptsarrayBlocorname(viaMkFormScope) and an optionalbuildWhen.rebuildOnValueChange: trueopts in to also rebuilding when any row's value changes (for UI that displays the values).- **
MkFormSelector<R>—BlocSelectorfor form state (e.g.(s) => s.canSubmit), so widgets that need one flag no longer rebuild on every keystroke (MkFormState.valueschanges constantly).
Changed #
- All field widgets are now Flutter
FormFields. A new base classMkFormField<T>(withMkFormFieldState<T>) is shared byMkTextField,MkEmailField,MkPasswordField,MkCheckboxField,MkRadioGroup,MkDropdownFieldandMkMultiSelectSearchDropdown. Inside aForm,validate(),save(),reset(),onChangedandforceErrorTextnow work with them, and every field acceptsonSaved:andforceErrorText:. The bloc remains the source of truth, and fine-grained rebuilds are preserved (typing still doesn't rebuild theTextField). MkEmailFieldandMkPasswordFieldnow extendMkTextField(instead of wrapping it).MkDropdownFieldnow renders aDropdownButtonin anInputDecorator(whatDropdownButtonFormFielddoes internally) so there is exactly oneFormFieldper field.- Reading
widget.enabled/widget.autovalidateModeon a field now yieldsFormField's members (bool/ alwaysAutovalidateMode.disabled). The constructor parameters are unchanged; the autovalidate override is available aswidget.mkAutovalidateMode.restorationIdis now also forwarded toFormField.
Performance #
populateValues(and thereforeloadInitialData) now applies the payload in one batch: one aggregate recompute and one form emission instead of one per field. 2,000 fields: ~480 ms → ~5 ms.validateControls/validateStepandsetFieldErrorsare batched the same way (validating a 20-field step in a 2,000-field form: ~12 ms → ~2 ms).
Fixed #
MkDropdownFieldandMkMultiSelectSearchDropdownshowed the label and the hint at the same time (the hint was drawn on top of the label). They now follow the MaterialTextFieldrule: with a label, the hint stays hidden until the label has floated (field focused, orfloatingLabelBehavior: always); without a label the hint is always shown. The hint keeps its layout space, so the field doesn't change height. The multi-select now also reportsisEmpty/isFocusedto itsInputDecorator, so its label floats correctly when focused.nextStep()/goToStep()could skip a step when called twice before validation finished (double tap, slow async validators): both calls advanced. Navigation is now serialized; a call made while another is in flight is ignored and returnsfalse, which also prevents a double route push.MkTextField(andMkEmailField/MkPasswordField) reverted or dropped typed characters whenever a parent rebuilt on each keystroke — most visibly rows inside a dynamic array (typing lagged, text disappeared, values lagged). The field skips rebuilds for value-only changes, which left itsBlocBuilder's cached state one keystroke behind; the next parent rebuild wrote that stale value back into the text controller. It now always renders from the bloc's live state.
Docs #
- New
doc/folder: 16 guides, each with a complete runnable example, including a Performance guide. - Fixed the README/
llms.txtstep-form snippet:BlocBuilder<MyStepBloc, MkStepFormState>does not compile (the declared state type isMkFormState); useMkFormStateBuilder<MkStepFormState>. - The dynamic-array example now uses
MkArrayBuilder.
1.2.1 - 2026-09-30 #
Docs #
- Documented
MkFormLocalizations.current's role as a no-BuildContextfallback only (used byMkValidators, which run in the bloc layer) — every built-in widget already resolves messages throughMkFormLocalizationsDelegate/Localizations.offirst, the same patternflutter_localizationsuses. Clarified in code doc comments (form_localizations.dart,validation.dart),README.md, andllms.txt. No behavior change. README.mdandllms.txtnow documentMkFieldSelector<T, R>andMkFieldBloc.isReadOnly/setReadOnly, both added in 1.2.0 but missing from user-facing docs until now.- Updated stale
mk_forms: ^1.x.xinstall snippets inREADME.md/llms.txtto^1.2.1.
1.2.0 - 2026-09-27 #
Added #
MkFieldSelector<T, R>— aMkFieldBuildersibling analogous toflutter_bloc'sBlocSelector: rebuilds only when a value derived fromMkFieldState<T>viaselectoractually changes (e.g. juststate.error), instead of on every field-state emission. Same field resolution (fieldBlocornamevia the nearestMkFormScope) andhideWhenInvisiblebehavior asMkFieldBuilder; the builder receives(context, selected, fieldBloc).MkFieldBloc.isReadOnly— a new bloc-level flag (constructor param, defaultfalse, plussetReadOnly(bool)) that excludes a field from validation entirely: while read-only, no validator (sync or async) runs,error/isValidatingstay cleared, andisValidis alwaystrue— so a read-only field never blocks the enclosingMkGroupBloc/MkFormBlocfrom being valid. Turning it on clears any existing error immediately; turning it off re-validates against the current value. This is independent of the existing, purely presentationalreadOnlyprop onMkTextField/MkEmailField/MkPasswordField(which only blocks typing and is unrelated to validation).
1.1.1 - 2026-09-27 #
Fixed #
- Memory leak: dynamically removing a control from a live
MkGroupBlocorMkArrayBlocnow closes it by default.MkGroupBloc.removeControl/addControlandMkArrayBloc.removeAt/remove/clearpreviously detached a removed control's listeners from its parent but never calledclose()on the removed control itself — every runtime "remove a row" / "remove a field" operation leaked one un-closed control, and if that control had an activeaddDependencyon another still-live control, it stayed reachable (and kept being revalidated) forever. Whole-form/whole-array teardown viaclose()was never affected — only dynamic partial removal.- All four methods gain an optional
dispose/disposeReplacedparameter (defaulttrue, matching the fixed behavior) for the rare case of intentionally re-parenting a control from one array/group to another without disposing it. removeAt/removeare unchanged in what they return (the removed control).removeControlandMkArrayBloc.clear()now return what they removed (removeControl: the removed control, previouslyvoid;clear(): the removedList<C>, previouslyvoid) so a caller passingdispose: falsecan still reach them.addControl/registerControlre-registering the same control instance under the key it already occupies no longer closes it — only a genuine replacement (a different instance) is disposed.MkFormBloc.registerControl/unregisterControlgained the samedisposeReplaced/disposeopt-out asMkGroupBloc, andunregisterControlnow returns the removed control (previouslyvoid) — needed for the common "toggle a conditional field off and back on" pattern, where the same field instance is unregistered and later re-registered rather than disposed.
- All four methods gain an optional
Performance #
MkValidators.pattern()now compiles itsRegExponce, when the validator is constructed, instead of once per validation call.
1.1.0 - 2026-09-24 #
Added #
- Extensible form state via generics.
MkFormState<T>carries a single typedextrafield, so one form can track several independent API calls (upload, availability check, OTP) or extra statuses — with no state subclass to write. See "Extending Form State" in the README.MkFormBloc<T>/MkStepFormBloc<T>— generic over the sameT;stateis already typed, with no downcast in application code. Both acceptinitialExtra/extraReset/isExtraBusydirectly, so an extras-using bloc doesn't have to hand-build itsinitialState:fromcontrols.MkFormState.updateExtra(fn)— shorthand forcopyWith(extra: () => fn(extra)).MkFormState.copyWith'sextraparameter is a zero-arg function (T Function()?), likesubmissionError, so a nullableextracan be explicitly cleared tonull—copyWith(extra: () => null)— distinct from omitting it.MkFormState.isBusy— reflectsisSubmittingplusisExtraBusy(extra)(set once via the constructor);canSubmitandMkFormBloc.submit()respect it.MkFormState.reset(...)— regeneratesextraviaextraReset(set once via the constructor), or leaves it unchanged if omitted.MkFormStateBuilder<S extends MkFormState>andMkFormStateWidgetBuilder<S>— unchanged in shape; used asMkFormStateBuilder<MkFormState<YourExtra>>.MkFormBuilderis unchanged.- Every pre-existing "no extras" form needs no source changes: omitting the type argument (
extends MkFormBloc,extends MkStepFormBloc) still compiles.
- Field-aware validation messages. Built-in validators (
required,email,minLength,maxLength,min,max,pattern,match) now produce messages that name the field, e.g."Email is required."instead of"This field is required".- This changes default text, not just adds an option. Any field registered in an
MkGroupBloc— which includes every field insideMkFormBloc/MkStepFormBloc, the common case — is auto-labeled and gets the new field-aware message fromMkDefaultEnglishMessages/MkSpanishMessages. A field is only unaffected if it's never added to a group and has no explicitlabel. - A field's label is auto-derived from its key in the controls map (
'confirmPassword'->"Confirm password"). Override with an explicitlabel:onMkFieldBloc's constructor when the auto-derived label isn't right. - Labels are derived from the (typically English) identifier used as the controls-map key, so a non-English
MkFormMessages(e.g.MkSpanishMessages) will show that label alongside its own translated sentence (e.g."Confirm password no coincide.") unless every field is given an explicit, translatedlabel:. MkFormMessagesgainedresolveField(errorCode, field, [params])plus 8 new overridable*Field(...)methods. They default to returningnullon the abstractMkFormMessagesclass — an app that implementsMkFormMessagesdirectly (the "entirely custom language" pattern this class's own doc comment describes) is unaffected unless it opts in by overriding them.MkDefaultEnglishMessagesandMkSpanishMessagesprovide concrete overrides, so apps using those as-is get the new text automatically.- Migration note: if you extend
MkDefaultEnglishMessagesorMkSpanishMessagesand override only the base methods (required(),email(), etc. — the pattern in this package's own README), your override is still used for fields with no label, but a labeled field falls back to the built-in field-aware text, not your override. Also override the corresponding*Fieldmethod (or overrideresolveFieldto returnnull) to keep your customization for labeled fields too. resolve()and every pre-existingMkFormMessagesmethod are unchanged.- A field that's never registered in a group and has no explicit
labelbehaves exactly as before this change.
- This changes default text, not just adds an option. Any field registered in an
Fixed #
MkFormBloc.reset()dropped subclass state by emitting a plainMkFormState. Resettingextrais now built intoMkFormState.reset()viaextraReset, andMkStepFormBlocno longer needs its ownreset()/onUpdateFormState()overrides.
Changed #
- This replaces the subclassing-based extensible-state design from the previous unreleased 1.1.0 draft (never published). If you were tracking that draft: subclass
ProfileFormState extends MkFormStatebecomesMkFormBloc<ProfileExtras>withextra: const ProfileExtras();state.extrasbecomesstate.extra.
1.0.1 - 2026-09-20 #
Added #
MkFormBuilder— Rebuilds fromMkFormState(with optionalformBlocandbuildWhen), mirroringMkFormListener.- All field widgets now expose the parameters of the Flutter widget they wrap, so they can be customized without rebuilding them:
MkTextField,MkEmailField,MkPasswordField: everyTextFieldparameter (inputFormatters,cursor*,autofillHints,contextMenuBuilder,onTap,onEditingComplete,focusNode,buildCounter,undoController, …) exceptcontroller.focusNodemay be supplied (it is never disposed by the widget);enabledcombines with the bloc's state.MkPasswordFieldgainsinitiallyObscured.MkCheckboxField: everyCheckboxListTileparameter (tristate,fillColor,shape,side,controlAffinity, …), plusonChanged,enabled,errorStyle,errorPadding.isErrordefaults to "an error is shown".MkDropdownField: everyDropdownButtonFormFieldparameter that is not aFormFieldmember (selectedItemBuilder,isExpanded,icon,dropdownColor,menuMaxHeight, …), plusenabled.MkRadioGroup: theRadioListTile/Radioparameters at group level,MkRadioOption.secondary,onChanged,enabled,highlightSelected,errorStyle.MkMultiSelectSearchDropdown:InkWell,InputChip, bottom sheet and dialog parameters (chip*,sheet*,dialog*), plus localizable labels (doneLabel,selectAllLabel,clearAllLabel,moreLabelBuilder),showSelectAll,icon,hintStyle,onChanged,enabled.
- Only parameters available in Flutter 3.41 are exposed (the version this package's own code already requires).
Fixed #
MkMultiSelectSearchDropdownonly rendered the first picked item: the modal handed the field its own mutable list, so later picks looked like no-ops to the bloc. It now reports a copy.MkFormBloc/MkStepFormBlocleaked a second, never-closedMkGroupBloc(and one extra listener per control) while building their initial state.MkFormBloc.submit()could runonSubmittwice when called again while validation was still in progress.- Editing a field while async validation was in flight made
submit()silently do nothing. A supersededMkFieldBloc.validate()now resolves to the outcome of the newer validation. - Mapping server errors onto fields inside
onSubmitFailure(or touching a field) no longer wipes the failure state; only real value edits clear it. MkValidators.emailrejected valid addresses with hyphenated domains and accepted trailing garbage, commas anda@b.c@@@. It is now anchored and follows the HTML5 rule.dependsOnno longer runs async validators at construction, no longer validates twice per dependency change, and debounces the async part likechangeValue. Dependents also detach from their dependency onclose(), and group/array dependencies only trigger when their contents actually change.- A throwing async validator no longer shows the raw exception text (hostnames, stack details). The field gets a generic localized message (
MkErrorCodes.asyncValidationFailed, English and Spanish included) and the error is reported throughonError/BlocObserver. isDirtycompares collection values (lists, sets, maps) by contents instead of identity.- Async work finishing after
close()(submit(),validate(),nextStep(),goToStep(),validateStep(),loadInitialData(),reset()) no longer throws "Cannot emit new states after calling close". MkRadioGroup(vertical layout) ignoredMkRadioOption.enabledand the bloc's enabled state visually; disabled options now render as disabled.- The multi-select modal title could overflow next to the Done button; it now ellipsizes.
Performance #
- Bulk operations (
touch,reset,markFormSubmitted,validate) on groups, arrays and forms recompute the aggregate once instead of once per child.submit()on a 1000-field form went from ~315 ms to ~16 ms andreset()from ~105 ms to ~1 ms. - Aggregate flags and nested values are read from cached state instead of re-walking the subtree.
- State listeners are only notified when a control's state actually changed.
MkTextFieldno longer rebuilds on every keystroke when nothing visible changes.- Added
MkFieldBloc.changeValue(value, markTouched: true); checkbox, radio and dropdown widgets use it to emit (and rebuild) once per interaction instead of twice.
Changed #
- Minimum versions corrected to Flutter
>=3.41.0/ Dart>=3.9.0. The code already required them (AutovalidateMode.onUserInteractionIfErrorships in 3.41,RadioGroupand the dropdown'sinitialValuein 3.35); the previous>=3.10.0made older Flutter fail at compile time instead of at version resolution. MkFormScopeis now aStatelessWidgetbuilt onBlocProviderinstead of a customInheritedWidget, with the same two constructors:MkFormScope(create:, lazy:)creates and closes the bloc,MkFormScope.value(value:)exposes an existing one without closing it. Breaking:MkFormScope(formBloc: x)is nowMkFormScope.value(value: x).MkFormScope.ofno longer registers a rebuild dependency (useMkFormBuilder).MkFieldBloc.validate({cascadeDependents}): the parameter is deprecated and ignored; dependents are revalidated when the dependency's value changes.- Upgraded
equatableto^3.0.0. - Dependencies bumped:
bloc ^9.2.1,flutter_bloc ^9.1.1,meta ^1.19.0(dev:bloc_test ^10,mocktail ^1.0.5,flutter_lints ^6). Removed the unusedrxdartdependency. - Tree shaking verified. Unused widgets, validators, the step-form bloc and the Spanish messages are removed from release builds; the package also builds with
flutter build web --wasmand keeps Flutter's icon tree shaking intact (see README, Tree Shaking & Bundle Size).
1.0.0 - 2026-09-19 #
🎉 Initial stable release #
Core Architecture
MkFieldBloc<T>— Reactive field BLoC managing value, sync/async validation, dirty/touched/pristine state, debouncing, and cross-field dependencies.MkFormBloc— Abstract coordinator BLoC managing aggregated validation, submission lifecycle, and automatic submission-error clearing on edit.MkSimpleFormBloc— LightweightMkFormBlocwith an inlineonSubmitclosure — no subclass required for simple forms.MkStepFormBloc— Multi-step wizard BLoC with granular per-step validation gating, multi-route state preservation, and API draft hydration.MkSimpleStepFormBloc— Inline-closure variant ofMkStepFormBloc.MkGroupBloc— Groups named controls into an independently validatable subform.MkArrayBloc<C>— Dynamic array of form controls with add, insert, remove, and move operations.
Validation
- Built-in
MkValidators:required,email,minLength,maxLength,min,max,pattern,match,matchControl,compose,composeAsync. - Async validators with configurable debounce time and automatic stale-request cancellation.
- Cross-field
dependsOn— automatic revalidation when a dependency field changes, without recursion loops. - Server error mapping via
setFieldErrors(Map<String, String>). - Partial-form validation:
validateGroup(name),validateControls([names]),validateStep([step]).
AutovalidateMode Support
AutovalidateMode.always— errors shown immediately, even on pristine fields.AutovalidateMode.onUserInteraction(default) — errors shown after touch or submit attempt.AutovalidateMode.onUnfocus— errors shown after field blur or submit attempt.AutovalidateMode.onUserInteractionIfError— same asonUserInteraction.AutovalidateMode.disabled— errors shown only after submit attempt.- Configurable at
MkFormScopelevel (scope-wide default) or per-field (override). MkFieldState.shouldShowError([AutovalidateMode])for custom widget integration.
Form State & Lifecycle
MkFieldState<T>:value,initialValue,error,isDirty,isTouched,isValidating,isValid,isInvalid,isPure,wasFormSubmitted,showError,shouldShowError([mode]).MkFormState:isValid,isValidating,isDirty,isTouched,isSubmitting,isSuccess,isFailure,submissionError,values.MkStepFormState: extendsMkFormStatewithcurrentStep,totalSteps,completedSteps,isLoadingInitialData,redirectedFromStep,invalidStepIndex.populateValues(data, {markPristine: true})— hydrate fields from API response without triggering premature errors.loadInitialData(loader, {resolveInitialStep})— async draft loading with smart redirect to earliest invalid step.reset()— resets form and all controls to their initial values.
UI Widgets
MkFormScope—InheritedWidgetproviding form bloc and defaultAutovalidateModeto descendants. Context extensions:context.mkForm,context.mkField<T>(name),context.mkGroup(name),context.mkAutovalidateMode.MkFormListener— Side-effect listener for submission status withonSubmitting,onSuccess,onFailurecallbacks.MkFieldBuilder<T>— Fine-grained BlocBuilder for a single field with optionalbuildWhen.MkSubformBuilder— Isolated BlocBuilder for a namedMkGroupBloc; changes in one subform never rebuild siblings.MkTextField— Material text field with automatic focus/blur tracking and controller sync.MkEmailField— Pre-configured email keyboard, autofill, and format validation.MkPasswordField— Obscured input with show/hide eye toggle.MkCheckboxField— Checkbox with error label and custom label widget.MkRadioGroup<T>— Vertical/horizontal/wrap layout; supports customoptionBuilder.MkDropdownField<T>— Single-selection dropdown.MkMultiSelectSearchDropdown<T>— Multi-selection with live search, chips, select-all/clear, bottom-sheet or dialog.
Internationalization (i18n)
MkFormLocalizations.delegate— standardflutter_localizationsintegration.- Built-in English (
DefaultEnglishMessages) and Spanish (SpanishMessages) message sets. - Global override:
MkFormLocalizations.current = MyMessages(). - Scoped override:
MkLocalizationScope(messages: ..., child: ...). - Dynamic runtime locale switching — validation errors update instantly without UI jank.
MkErrorCodes— error code constants for i18n message resolution.
Testing
- 68 unit and widget tests covering field blocs, form lifecycle, step forms, subform validation, array blocs, validators, localization, all field widgets, and all
AutovalidateModevariants.