How AppFlowy Manages Workspace and View Hierarchy in Its UI: A Deep Dive into the Folder Architecture
AppFlowy implements a tree-based workspace and view hierarchy using the FolderManager service and collab_folder crate, where each view stores its parent's ID to form a traversable tree structure synchronized via CRDTs across devices.
AppFlowy organizes content through a folder-based collaborative model that treats workspaces as top-level containers and views as nested documents, boards, or databases. The workspace and view hierarchy is managed by the FolderManager struct in the flowy-folder crate, which bridges UI interactions with the underlying Folder object from the collab_folder library. This architecture enables real-time collaboration by ensuring all hierarchy mutations update a shared CRDT-based state that syncs across local persistence and cloud services.
The FolderManager: Central Hub for Hierarchy Operations
The FolderManager in frontend/rust-lib/flowy-folder/src/manager.rs serves as the core service coordinating all workspace and view hierarchy changes. It maintains a thread-safe reference to the shared Folder state using ArcSwapOption<RwLock<Folder>>, allowing concurrent read access while ensuring exclusive write access during mutations.
All hierarchy modifications flow through this manager, including view creation, parent reassignment, trash operations, and tree traversal queries. The FolderManager translates high-level UI requests into low-level Folder operations, persists changes via the collab backend, and emits notifications to update the Flutter frontend in real time.
Workspace Representation and Retrieval
Workspaces in AppFlowy are represented by two primary structures that bridge the internal model and the UI protocol.
WorkspacePB (defined in frontend/rust-lib/flowy-folder/src/entities/workspace.rs) is the Protobuf DTO sent to clients, containing the workspace ID, name, list of top-level views, and creation timestamp. The internal Workspace model (from the external collab_folder crate) stores the canonical state, with one workspace per user owned by the Folder object.
To retrieve workspace data, the FolderManager provides two key methods:
get_current_workspace(): Returns the currently activeWorkspacePBby reading theFolderstate and converting the internalWorkspacemodel (lines 131-151 inmanager.rs).get_workspace_pb(): Returns a completeWorkspacePBcontaining all views including private ones, useful for workspace-level exports or administrative operations (lines 536-559 inmanager.rs).
View Hierarchy: Tree Structure and Parent References
The view hierarchy forms a tree where every view is a node containing a parent_view_id field pointing to its immediate ancestor. This parent-reference pattern enables efficient traversal both upward (to ancestors) and downward (to descendants) without maintaining complex adjacency lists.
ViewPB (defined in frontend/rust-lib/flowy-folder/src/entities/view.rs) represents views in the UI layer, containing:
id: Unique identifier (UUID)parent_view_id: The parent node's ID (root views use the workspace ID)name,layout(Document, Board, Grid, Calendar),icon- Child view lists, favorite flags, and lock status
The crate provides three serialization helpers for different traversal depths:
view_pb_without_child_views(): Serializes a view as a leaf node without children (lines 91-107).view_pb_with_child_views(): Embeds only the first level of children, optimizing for standard UI display (lines 27-48).view_pb_with_all_child_views(): Recursively nests all descendants, used for deep exports or publishing (lines 49-84).
Core Hierarchy CRUD Operations
All tree mutations operate on the shared Folder instance managed by FolderManager, ensuring atomic updates to the CRDT state.
Creating Views with Parent Relationships
create_view_with_params() (lines 661-716 in manager.rs) handles view creation by:
- Invoking a layout-specific
FolderOperationHandlerto create the underlying collab object (document, database, etc.). - Inserting the view into the
Foldertree using the suppliedparent_view_idto determine position. - Optionally setting the view as the current active view and assigning it to a section (Public or Private).
insert_views_with_parent() (lines 562-580) handles bulk insertions, rewriting the parent_view_id of each supplied view or defaulting to the most recently edited view if no parent is specified.
Moving Views Within and Between Parents
AppFlowy supports two distinct move semantics:
move_view()(lines 442-470): Reorders a view within its current parent by translating UI indices to actual indices in the parent's child list, then callingFolder::move_viewto update the CRDT.move_nested_view()(lines 904-934): Moves a view to a different parent, updating theparent_view_idfield, reordering the target sibling list based onprev_view_id, and optionally changing the view's visibility section (private to public or vice versa).
Traversing Ancestors and Descendants
get_view_pb() (lines 671-724) resolves a view by ID, filters out trashed or private views based on permissions, and returns a ViewPB with first-level children using view_pb_with_child_views().
get_view_ancestors_pb() (lines 696-712) walks upward from a given view via parent_view_id until reaching the workspace root, returning the breadcrumb path from root to leaf.
move_view_to_trash() (lines 818-859) performs cascading deletion by marking a view and all its descendants as trashed, removing them from favorites, and notifying observers to update the UI.
Data Transfer Objects: WorkspacePB and ViewPB
The separation between internal models and presentation models ensures backward compatibility and network efficiency.
WorkspacePB(lines 15-28 inentities/workspace.rs): Protobuf definition for workspace serialization, containing metadata and top-level view IDs.ViewPB(lines 39-88 inentities/view.rs): Comprehensive Protobuf struct capturing view metadata, layout type, and nested children.
Conversion between internal collab_folder types and these PBs happens in the manager layer, allowing the core logic to evolve independently of the UI protocol.
Real-Time Synchronization and Notifications
Whenever the workspace and view hierarchy changes, FolderManager emits folder notifications through the observer system defined in frontend/rust-lib/flowy-folder/src/manager_observer.rs.
Key notification patterns include:
DidMoveViewToTrash: Broadcast when views are soft-deleted, triggering UI removal.DidUpdateWorkspaceSetting: Fired when workspace metadata changes.notify_child_views_changedandnotify_parent_view_did_change: Helper functions that ensure both the moved view and its parent containers refresh in the UI.
The underlying Folder object is a collab object (CollabType::Folder) initialized in FolderManager::make_folder or initialize_after_sign_in. All mutations update the CRDT document, enabling real-time collaboration across devices while maintaining local persistence.
Practical Code Examples
Example 1: Creating a Document View Under a Parent
use flowy_folder::entities::{CreateViewParams, ViewLayoutPB};
use uuid::Uuid;
// Assume we already have a FolderManager instance `manager`
let parent_id = Uuid::parse_str("5f8e4a1d‑c5b2‑4a1e‑a0f7‑123456789abc")?;
let params = CreateViewParams {
parent_view_id: parent_id,
name: "My new doc".to_string(),
layout: ViewLayoutPB::Document,
view_id: gen_view_id(),
initial_data: ViewData::Data(vec![]),
meta: Default::default(),
set_as_current: true,
index: None,
section: Some(ViewSectionPB::Public),
extra: None,
icon: None,
};
let (view, _) = manager.create_view_with_params(params, true).await?;
println!("Created view {}", view.id);
This creates a view whose parent_view_id points to the supplied parent, automatically inserting it into the folder tree.
Example 2: Moving a View to a Different Parent
use flowy_folder::entities::MoveNestedViewParams;
// Move view `v1` under new parent `v2`, placing it after sibling `v3`
let params = MoveNestedViewParams {
view_id: Uuid::parse_str("v1‑uuid")?,
new_parent_id: Uuid::parse_str("v2‑uuid")?,
prev_view_id: Some(Uuid::parse_str("v3‑uuid")?),
from_section: None,
to_section: Some(ViewSectionPB::Public),
};
manager.move_nested_view(params).await?;
The function updates parent_view_id of v1, reorders the sibling list, and sends the appropriate notifications to connected clients.
Example 3: Retrieving the Full Descendant Tree
let view_id = "5f8e4a1d‑c5b2‑4a1e‑a0f7‑123456789abc";
let root_pb = manager.get_view_pb(view_id).await?; // first-level children
let full_tree = view_pb_with_all_child_views(
Arc::clone(&folder.get_view(view_id).unwrap()),
&|id| manager.get_views_belong_to(id).await.unwrap(),
);
view_pb_with_all_child_views recursively collects all nested children, useful for exporting a complete page hierarchy or generating static sites.
Summary
- Tree Structure: AppFlowy uses a parent-reference model where each view stores its
parent_view_id, forming a traversable hierarchy within a workspace. - Central Service: The
FolderManagerinmanager.rscoordinates all CRUD operations, protecting shared state withArcSwapOption<RwLock<Folder>>. - DTO Layer:
WorkspacePBandViewPBin theentitiesmodule provide protocol-buffer interfaces between the Rust backend and Flutter UI. - Traversal Methods: Specialized helpers like
view_pb_with_all_child_viewsandget_view_ancestors_pbsupport both shallow UI rendering and deep tree exports. - Real-Time Sync: All hierarchy changes update a CRDT-based
Folderobject, ensuring immediate synchronization across devices via the collab infrastructure.
Frequently Asked Questions
How does AppFlowy handle moving a view between different parents?
AppFlowy uses the move_nested_view() method in FolderManager (lines 904-934 of manager.rs). This function updates the view's parent_view_id to the new parent's ID, recalculates the position within the target sibling list using prev_view_id, and optionally transfers the view between sections (Public or Private). The operation is atomic within the CRDT and triggers notifications to all connected clients.
What is the difference between ViewPB and the internal View model?
ViewPB is a Protobuf DTO defined in entities/view.rs that serializes view data for transmission to the Flutter frontend, including all metadata and optional child views. The internal View model (from the collab_folder crate) is the in-memory CRDT representation optimized for collaborative editing. The FolderManager translates between these representations when handling UI requests.
How does AppFlowy prevent data loss when deleting views?
Rather than immediate deletion, AppFlowy implements a soft-delete mechanism via move_view_to_trash() (lines 818-859 in manager.rs). This function marks the target view and all its descendants with a trashed flag, removes them from the favorites list, and notifies observers. Trashed items remain in the database and can be restored until permanently purged by user action.
Where is the workspace and view hierarchy state stored?
The canonical state lives inside the Folder object from the collab_folder crate, managed as a collab document (CollabType::Folder). During initialization (FolderManager::make_folder), this object loads from local RocksDB persistence or fetches from the cloud sync service. All hierarchy mutations modify this shared CRDT document, ensuring consistency across local storage and remote collaboration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →