This library enables seamless content sharing for the AppFlowy text editor across devices and users. It leverages the CRDT (Conflict-free Replicated Data Type) structures from the yrs library to merge changes from multiple devices consistently, ensuring identical results regardless of the order or frequency of updates.
Full offline support
More details about the demo here with custom synchronization: https://github.com/Musta-Pollo/custom_supabase_drift_doc_sync
Link: https://habitmaster-e52e9.web.app/
demo_recording.mp4
The demo is slightly longer because it is a live demonstration that requires turning the Wi-Fi on and off. This demo functions well across all other Flutter platforms and Wear OS when properly configured.
Init the plugin inside main:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await AppflowyEditorSyncUtilityFunctions.initAppFlowyEditorSync();
runApp(App());
}
Override three methods to handle document update storage and retrieval:
@Riverpod(keepAlive: true)
class EditorStateWrapper extends _$EditorStateWrapper {
...
@override
FutureOr<EditorState> build(String docId) {
final wrapper = EditorStateSyncWrapper(
syncAttributes: SyncAttributes(
/// Provide all editor updates or initialize the editor, save the updates
/// and return them.
/// See: [AppflowyEditorSyncUtilityFunctions]
getInitialUpdates: () async {
...
},
getUpdatesStream: ...,
saveUpdate: (Uint8List update) async {
...
},
),
);
return wrapper.initAndHandleChanges();
}
}
Pass the resulting EditorState to the AppFlowy text editor. See the example for details.
When creating a document, initialize it using one of the following methods from AppflowyEditorSyncUtilityFunctions.*
:
initDocument
initDocumentFromExistingDocument
initDocumentFromExistingMarkdownDocument
These methods set up the default document structure for future updates.
To use the plugin on the web you need to do copy the code from the package web/pkg
into you project web/pkg
folder. And provide html headers:
Cross-Origin-Opener-Policy=same-origin
web-header=Cross-Origin-Embedder-Policy=require-corp
Usefull resources:
- https://cjycode.com/flutter_rust_bridge/quickstart#3-run-it
- https://cjycode.com/flutter_rust_bridge/manual/integrate/template/setup/web
This is neccessary as the package relies on flutter_rust_bridge.
You can build these file using: flutter_rust_bridge_codegen build-web
The library builds on AppFlowy’s approach to convert transactions into structures compatible with the Rust-based yrs library, maintaining a synchronized copy of the text editor. Changes are reflected in yrs CRDT structures, ensuring consistent state across devices.
Look at diagrams:
However, CRDT alone may produce unexpected results when users’ changes interleave. For example:
- 111 - User A
- 222 - User A
- 333 - User A
- 111 - User B
- 222 - User B
- 333 - User B
When both devices sync, a naive CRDT merge might yield:
- 111 - User A
- 111 - User B
- 222 - User A
- 333 - User A
This is not user-friendly. To address this, each text node (line) includes additional attributes:
- deviceId
- timestamp
And pointers:
- prevId (for reflection and sorting)
- nextId (for CRDT reflection)
Using these, the plugin sorts nodes to produce a user-expected merge, such as:
- 111 - User A
- 222 - User A
- 333 - User A
- 111 - User B
- 222 - User B
- 333 - User B
Or:
- 111 - User B
- 222 - User B
- 333 - User B
- 111 - User A
- 222 - User A
- 333 - User A
For conflicts (e.g., multiple nodes with the same prevId), the sorting algorithm uses timestamp and deviceId to resolve ordering. This logic is implemented in rust/src/doc/utils/sorting.rs.
After updating the CRDT struture, an CRDT binary update is returned that should be then stored. After new binary updates come from a DB, these updates are then combined and form a CRDT document that is then compared with the current one and differences are applied to the current editor.
Code is written both in Dart and Rust.
Main files or folders are listed bellow:
-
src/doc:
document_service
- Service used for communicatinio between Rust and Dart part.utils
sorting.rs
- Sorts the current CRDT state into user expected statesorting_test.rs
and sorting_test2.rs - Contains tests generated by AI and reviewed for their validity.
operations
block_ops.rs
- Handles insersts, deletes, updates, and moves.delta_ops.rs
- Handle operations releated to text that is stored in Delta Format.update_ops.rs
- Handles applying CRDT updates and extraction of current CRDT state into a state that Dart side understands.
conversions
conversion.rs
- Responsible for conversions between JSON and yrs formats.
Main files or folders are listed bellow:
-
lib:
editor_state_sync_wrapper.dart
- Listenes for updates from editor or DB and puts everything together.appflowy_editor_sync_utility_functions.dart
- Contains usefull function for creating new documents and merging updates.extensions
- Contains files adding extentions to default Appflowy Editor classes or classes generated from the Rust sidedocument_service_helpers
document_service_wrapper
- Wrapper aroundDocumentService
from Rust that gurateese that CRDT structure on the other side is affected only by one call at a timediff_deltas.dart
- Function for computing Deltas (Delta Format) with adjustments to make it work with YText format.document_rules
- Validates the new editor state and if neccessary fixes it.
core
- Helper structures batching and update clock.src/rust
- Contains generted code by flutter_rust_bridge.
- Setup Flutter on local system
cd example
flutter run