Skip to main content
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.
final Document document;

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:
editorState.reload();
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

Build docs developers (and LLMs) love