Lifecycle Callbacks and Events
FdcDataSet exposes typed callbacks for record lifecycle policy, state transitions, validation and errors, background work, field changes, new-record initialization, and cursor movement.
Use these callbacks when behavior belongs to the data lifecycle and must run regardless of whether the operation was started by a grid, a standalone editor, or application code.
final customers = FdcDataSet(
fields: customerFields,
adapter: adapter,
beforePost: (dataSet) {
// Last supported veto/adjustment point before calculated values
// and validation are evaluated.
},
afterPost: (dataSet) {
// The local record transition has completed and the dataset is
// already back in browse state.
},
onFieldChanged: (dataSet, field, oldValue, newValue) {
// React to field-level changes in the active edit buffer.
},
onError: (dataSet, errors, cause) {
// Report dataset-visible failures.
},
);
Lifecycle callback families
Open and close
| Callback | When it runs | Can veto? |
|---|---|---|
beforeOpen | Before rows are loaded or replaced | Yes |
afterOpen | After rows are committed and the dataset is in browse | No rollback |
beforeClose | Before records, query state, paging state, selection, and edit buffers are cleared | Yes |
afterClose | After the dataset has entered closed | No rollback |
beforeOpen is also used by local loadRows() because loading local rows starts a new dataset data lifecycle.
Edit, insert, post, delete, and cancel
| Callback | When it runs | Can veto? |
|---|---|---|
beforeEdit | Before the edit buffer is created | Yes |
afterEdit | After the edit buffer exists and state is edit | Notification |
beforeInsert | Before the new record is inserted or defaults are applied | Yes |
afterInsert | After the record is active, initialized, visible, and state is insert | Throwing rolls the in-progress insert back |
beforePost | Before calculated values and validation | Yes |
afterPost | After local posted values are applied and state is browse | No rollback |
beforeDelete | Before an existing record is removed or marked deleted | Yes |
afterDelete | After the local delete transition and return to browse | No rollback |
beforeCancel | Before the active edit or insert buffer is discarded | Yes |
afterCancel | After values are restored or the unposted insert is discarded | No rollback |
Throw FdcDataSetAbortException from supported veto points to intentionally stop an operation through the dataset abort pipeline. Silent aborts stop the operation without producing the normal dataset error event.
Other DataSet events and callbacks
onStateChanged
Called synchronously whenever the dataset state value changes:
onStateChanged: (dataSet, previousState, currentState) {
log('$previousState -> $currentState');
},
The new state is already active when the callback runs. The callback executes inside the transition itself, before the corresponding after... callback completes and before the final notifyListeners() notification. Avoid starting re-entrant lifecycle operations from this callback.
onNewRecord
Use onNewRecord to initialize a newly inserted or appended record with values that need dataset context:
onNewRecord: (dataSet) {
dataSet.setFieldValue('status', 'Active');
dataSet.setFieldValue('created_at', DateTime.now());
},
It runs after the blank record has been created, inserted, activated, and placed in an insert edit buffer, but before afterInsert.
Assignments made through normal dataset APIs are part of the insert buffer. They are treated as initialization rather than user edits. Each actual value change may also emit onFieldChanged.
onFieldChanged
Called after a field value changes in the active edit buffer or through a direct field-level update operation:
onFieldChanged: (dataSet, field, oldValue, newValue) {
if (field.name == 'country') {
refreshStateOptions(newValue);
}
},
This is a notification event, not a veto point. If its handler changes another field, that nested change emits another onFieldChanged event.
beforeScroll and afterScroll
Use cursor events when application behavior depends on the active record:
beforeScroll: (dataSet, currentRecordNumber, targetRecordNumber) {
// May throw FdcDataSetAbortException to block movement.
},
afterScroll: (dataSet, previousRecordNumber, currentRecordNumber) {
// React after the active record changed.
},
Record numbers are 1-based visible record numbers. They are -1 when no active record exists.
Before cursor movement, the dataset first resolves an active edit as required by the navigation path. Therefore a dirty record may post, or a clean active edit may cancel, before the scroll callbacks execute.
Validation and dataset errors
onValidationError runs after post validation has produced one or more errors and after the dataset error store has been updated. The active edit remains available for correction.
onValidationError: (dataSet, errors) {
showValidationSummary(errors);
},
onError runs when the dataset records user-visible dataset errors:
onError: (dataSet, errors, cause) {
reportDataErrors(errors, cause: cause);
},
Silent FdcDataSetAbortException instances do not emit onError because they intentionally do not create dataset errors.
Work lifecycle events
Long-running dataset work exposes three callbacks:
onWorkStarted: (dataSet, work) {
showProgress(work.phase);
},
onWorkCompleted: (dataSet, work) {
hideProgress();
},
onWorkError: (dataSet, work, error, stackTrace) {
hideProgress();
},
Work phases include filter, sort, search, applyUpdates, load, and custom.
The generic work lifecycle is:
onWorkStarted
-> operation body
-> onWorkCompleted
On failure:
onWorkStarted
-> operation body throws
-> onWorkError
-> error continues through the normal dataset error pipeline
onWorkCompleted is not called after a failed work operation.
Exact execution order
The following sequences describe the observable callback/event order for the normal successful path.
Adapter-backed open()
onWorkStarted(load)
-> beforeOpen
-> onStateChanged(previous -> loading)
-> adapter load
-> rows/query/paging state committed
-> onStateChanged(loading -> browse)
-> afterOpen
-> notifyListeners
-> onWorkCompleted(load)
A failed adapter load emits onWorkError instead of onWorkCompleted, then continues through normal dataset error handling. State restoration depends on whether the operation was a fresh open, reload, or appended page load.
Local loadRows()
For synchronous local rows:
onWorkStarted(load)
-> beforeOpen
-> rows committed
-> onStateChanged(... -> browse), when state changes
-> afterOpen
-> notifyListeners
-> onWorkCompleted(load)
For asynchronous local rows, the dataset also enters loading before awaiting the rows and returns to browse after commit.
close()
beforeClose
-> records/query/selection/paging/edit state cleared
-> onStateChanged(... -> closed)
-> afterClose
-> notifyListeners
edit()
beforeEdit
-> edit buffer created
-> onStateChanged(browse -> edit)
-> afterEdit
-> notifyListeners
Calling edit() while already in edit or insert is a no-op.
insert() and append()
beforeInsert
-> blank record prepared and inserted
-> record activated
-> onStateChanged(browse -> insert)
-> onNewRecord
-> zero or more onFieldChanged events
-> inserted record view refreshed
-> afterInsert
-> notifyListeners
The public difference between insert() and append() is placement policy. Their callback order is the same.
If afterInsert throws, the new record is rolled back and the dataset returns to browse.
Field assignment during edit or insert
field write requested
-> value normalized/written to edit buffer
-> aggregate cache invalidated for affected fields
-> onFieldChanged for each actual changed field
-> notifyListeners
No event is emitted when the effective value does not change.
post()
The common validation portion is:
beforePost
-> calculated fields evaluated
-> record/field/storage validation
-> onValidationError, only when validation fails
On validation failure, posted values are not committed and afterPost does not run.
On successful local post:
beforePost
-> calculated fields
-> validation
-> changed values applied to record
-> record state resolved
-> edit buffer cleared
-> onStateChanged(edit|insert -> browse)
-> afterPost
-> notifyListeners
For an adapter-backed dataset in cachedUpdates mode, the local post order is the same in principle: the record transition completes locally, afterPost runs, and persistence waits for an explicit applyUpdates().
For an adapter-backed dataset in immediate mode:
beforePost
-> calculated fields
-> validation
-> local record transition
-> immediate post marked pending
-> onStateChanged(edit|insert -> browse)
-> afterPost
-> notifyListeners
-> immediate apply scheduled
-> onWorkStarted(applyUpdates)
-> adapter apply
-> onWorkCompleted(applyUpdates)
afterPost therefore means that the local post transition has completed. It does not mean that an immediate adapter write has already finished.
delete()
For an existing record:
beforeDelete
-> record removed locally or marked deleted
-> onStateChanged(... -> browse), when state changes
-> afterDelete
-> notifyListeners
-> immediate apply scheduled when update mode is immediate
An unposted insert deleted while still in insert mode is discarded directly. It does not fire beforeDelete, afterDelete, beforeCancel, or afterCancel.
cancel()
For an edited existing record:
beforeCancel
-> original values and record state restored
-> view rebuilt around the record
-> edit buffer cleared
-> onStateChanged(edit -> browse)
-> afterCancel
-> notifyListeners
For an unposted insert:
beforeCancel
-> inserted record removed
-> edit buffer cleared
-> onStateChanged(insert -> browse)
-> afterCancel
-> notifyListeners
Cursor navigation
For a normal cursor move with no active edit:
beforeScroll
-> current record changes
-> afterScroll
-> notifyListeners
When an edit is active, the dataset resolves it first. A dirty edit is posted; a clean edit is canceled. Only after that lifecycle completes does cursor movement continue.
Filter, search, and sort work
Filter, search, and sort operations use the dataset work lifecycle:
onWorkStarted(filter | search | sort)
-> local view rebuild or adapter-backed query flow
-> cursor/view state committed
-> notifyListeners as required by the operation
-> onWorkCompleted(filter | search | sort)
On failure, onWorkError replaces onWorkCompleted and the exception continues through dataset error handling.
applyUpdates()
For adapter-backed changes:
onStateChanged(previous -> applyingUpdates)
-> notifyListeners
-> onWorkStarted(applyUpdates)
-> adapter apply
-> onWorkCompleted(applyUpdates)
-> backend-confirmed values/change states reconciled
-> onStateChanged(applyingUpdates -> resolved state)
-> notifyListeners
Application logic that needs to know that local editing finished should use afterPost or afterDelete. Logic that needs to know that adapter work finished should observe the work lifecycle and/or await applyUpdates().
Choosing the right callback
Use lifecycle callbacks for data policy and operation boundaries. Use onFieldChanged for reactions to data changes. Use onValidationError and onError for error presentation/reporting. Use work events for progress and long-running operation UX. Use scroll callbacks only for active-record behavior.
Do not put persistence transport logic in grid callbacks, and do not use a generic ChangeNotifier listener when a typed dataset event already expresses the transition you need.