The EditorState is the central state management class for AppFlowy Editor. It manages the document, selection, services, and undo/redo functionality.
Overview
EditorState encapsulates all the state and services required to render and interact with the editor:
The document structure to render
The current selection state
Editor services (selection, scroll, keyboard, input)
Undo/redo history management
Transaction handling
All mutations to the document should be applied through Transaction objects, not by directly modifying the document. This ensures proper undo/redo support and collaborative editing compatibility.
Creating an EditorState
From an Existing Document
final document = Document . fromJson (jsonData);
final editorState = EditorState (document : document);
Blank Document
// Creates a blank document with an empty paragraph
final editorState = EditorState . blank (withInitialText : true );
// Creates a completely empty document
final editorState = EditorState . blank (withInitialText : false );
Core Properties
document
The Document instance that represents the content structure.
selection
The current selection in the editor. Returns null when there is no active selection.
Selection ? get selection => selectionNotifier.value;
set selection ( Selection ? value) {
selectionNotifier.value = value;
}
The selection is backed by a PropertyValueNotifier, allowing widgets to listen for changes.
selectionNotifier
A ValueNotifier that broadcasts selection changes to listeners.
final PropertyValueNotifier < Selection ?> selectionNotifier;
editable
Controls whether the editor accepts user input.
bool get editable => editableNotifier.value;
set editable ( bool value) {
editableNotifier.value = value;
}
transaction
Creates a new Transaction for the current document.
Transaction get transaction {
final transaction = Transaction (document : document);
transaction.beforeSelection = selection;
return transaction;
}
Applying Transactions
The apply() method is the primary way to update the editor state:
Future < void > apply (
Transaction transaction, {
bool isRemote = false ,
ApplyOptions options = const ApplyOptions (recordUndo : true ),
bool withUpdateSelection = true ,
bool skipHistoryDebounce = false ,
}) async
Parameters
transaction : The transaction containing operations to apply
isRemote : Whether this transaction originates from a remote source (collaborative editing)
options : Controls undo/redo recording behavior
withUpdateSelection : Whether to update the selection after applying
skipHistoryDebounce : Whether to immediately seal the history item
Example Usage
final transaction = editorState.transaction;
transaction. insertText (
node,
0 ,
'Hello, world!' ,
);
await editorState. apply (transaction);
ApplyOptions
class ApplyOptions {
const ApplyOptions ({
this .recordUndo = true ,
this .recordRedo = false ,
this .inMemoryUpdate = false ,
});
final bool recordUndo;
final bool recordRedo;
final bool inMemoryUpdate;
}
Selection Management
Updating Selection with Context
Use updateSelectionWithReason() to provide context about why the selection changed:
await editorState. updateSelectionWithReason (
newSelection,
reason : SelectionUpdateReason .uiEvent,
);
SelectionUpdateReason
enum SelectionUpdateReason {
uiEvent, // Mouse click, keyboard event
transaction, // Insert, delete, format
remote, // Remote selection in collaborative editing
selectAll,
searchHighlight,
}
SelectionType
enum SelectionType {
inline, // Text selection within blocks
block, // Block-level selection
}
Undo/Redo System
The EditorState includes a built-in undo/redo system through UndoManager.
Configuration
final editorState = EditorState (
document : document,
minHistoryItemDuration : const Duration (milliseconds : 50 ),
maxHistoryItemSize : 200 ,
);
Accessing UndoManager
final undoManager = editorState.undoManager;
// Perform undo
if (undoManager.undoStack.isNonEmpty) {
await undoManager. undo ();
}
// Perform redo
if (undoManager.redoStack.isNonEmpty) {
await undoManager. redo ();
}
Transaction Stream
Listen to transaction events for real-time updates, useful for collaborative editing:
editorState.transactionStream. listen ((value) {
final (time, transaction, options) = value;
if (time == TransactionTime .before) {
// Transaction is about to be applied
} else {
// Transaction has been applied
}
});
TransactionTime
enum TransactionTime {
before, // Before transaction is applied
after, // After transaction is applied
}
Working with Nodes
Get Nodes in Selection
final selection = editorState.selection;
if (selection != null ) {
final nodes = editorState. getNodesInSelection (selection);
// Process nodes...
}
Get Node at Path
final node = editorState. getNodeAtPath ([ 0 , 1 ]);
if (node != null ) {
// Work with node
}
Get Selected Nodes
final selectedNodes = editorState. getSelectedNodes (
selection : editorState.selection,
withCopy : true , // Returns deep copies of nodes
);
Services
The editor state provides access to various services:
// Scroll service
final scrollService = editorState.scrollService;
// Selection service
final selectionService = editorState.selectionService;
// Renderer service
final renderer = editorState.renderer;
Lifecycle Management
Disposal
Always dispose of the EditorState when it’s no longer needed:
@override
void dispose () {
editorState. dispose ();
super . dispose ();
}
Checking if Disposed
if (editorState.isDisposed) {
// State has been disposed
return ;
}
Force Rebuild
To force the editor to rebuild without applying a transaction:
This triggers a notification on the document root, causing the entire editor tree to rebuild.
Best Practices
Always Use Transactions Never mutate the document directly. Always use transactions to ensure proper undo/redo support.
Dispose Properly Always call dispose() when the editor state is no longer needed to prevent memory leaks.
Listen to Changes Use selectionNotifier and transactionStream to react to state changes.
Check Editable State Check editable before allowing user interactions that modify the document.
Complete Example
class MyEditor extends StatefulWidget {
@override
State < MyEditor > createState () => _MyEditorState ();
}
class _MyEditorState extends State < MyEditor > {
late EditorState editorState;
@override
void initState () {
super . initState ();
// Create editor state with initial content
editorState = EditorState . blank (withInitialText : true );
// Listen to selection changes
editorState.selectionNotifier. addListener (_onSelectionChanged);
// Listen to transactions for collaborative editing
editorState.transactionStream. listen (_onTransaction);
}
void _onSelectionChanged () {
final selection = editorState.selection;
print ( 'Selection changed: $ selection ' );
}
void _onTransaction (( TransactionTime , Transaction , ApplyOptions ) value) {
final (time, transaction, _) = value;
if (time == TransactionTime .after) {
// Send to backend for collaborative editing
print ( 'Transaction applied: ${ transaction . operations . length } operations' );
}
}
Future < void > _insertText () async {
final selection = editorState.selection;
if (selection == null ) return ;
final node = editorState. getNodeAtPath (selection.start.path);
if (node == null ) return ;
final transaction = editorState.transaction;
transaction. insertText (node, selection.start.offset, 'Hello!' );
await editorState. apply (transaction);
}
@override
void dispose () {
editorState. dispose ();
super . dispose ();
}
@override
Widget build ( BuildContext context) {
return AppFlowyEditor (
editorState : editorState,
);
}
}
See Also