flutter_sync_tree 1.0.9
flutter_sync_tree: ^1.0.9 copied to clipboard
A high-performance, hierarchical synchronization framework with weighted progress and throttling for Flutter.
π² flutter_sync_tree
A robust, high-performance synchronization framework for Flutter and Dart.
Manage complex multi-stage data pipelines with weighted progress, intelligent throttling, and resilient flow control.
| Dependency Pipeline Primary + Late phase composition |
Firebase Cluster Parallel leaf nodes |
![]() |
![]() |
Why flutter_sync_tree? #
When syncing large datasets from Firebase or running multi-stage initialization, these problems arise:
Progress is a lie β A 1-item task and a 1,000-item task should not each represent 50% of the bar.
UI jank β Emitting thousands of state updates per second freezes the interface.
One listener, two views β You want a single root to watch for overall status, but you also need to know which specific node just failed or progressed. Without origin tracking, you'd need a separate listener per task.
Rigid operation types β Standard sync libraries give you success / failure. Real-world sync needs richer output: how many items were added vs. updated vs. already up-to-date vs. recovered after retry.
flutter_sync_tree solves all of these. Progress is weighted by actual workload, updates pass through a configurable throttle gate, every event carries its origin node, and SyncSummary accepts any string key you define.
Features #
| ποΈ Composite Tree | Nest SyncLeaf and SyncComposite nodes into arbitrarily deep hierarchies |
| βοΈ Weighted Progress | completedCount / totalCount across all children β not a naive average |
| β‘ Throttled Updates | Gate UI rebuilds by delta threshold and time interval |
| π Exponential Backoff | Automatic retry with jitter: baseDelay Γ multiplierβΏ |
| βΈοΈ Pause / Resume | Suspend mid-flight without losing state; resume from the same point |
| π Granular Stats | Per-node SyncSummary: add, update, remove, latest, recover |
| π― Origin Tracking | Every event carries the node that first triggered it |
| π¨ Flutter Native | ChangeNotifier built-in β drop into any ListenableBuilder |
| π² Structured Logs | Depth-aware console output that mirrors your tree |
Architecture #
Every unit in the tree is a SyncNode. The two concrete types share a unified interface:
SyncNode (abstract β lifecycle contract + ChangeNotifier)
βββ SyncLeaf<T> executes actual work, owns Throttler + RetryConfig
βββ SyncComposite orchestrates children, aggregates progress & summary
Example tree
root (SyncComposite)
βββ [Primary β parallel]
β βββ user_profile SyncLeaf 100 items ββββββββ high weight
β βββ app_settings SyncLeaf 1 item β low weight
βββ [Late β parallel, starts after primary]
βββ photo_gallery SyncComposite
βββ album_meta SyncLeaf
βββ hires_images SyncLeaf
Event Flow #
SyncComposite listens to every child and re-broadcasts aggregated events to the UI:
| Child Event | Parent Emits | Origin | Notes |
|---|---|---|---|
start |
progress |
child | Signals a specific task has begun |
progress |
progress |
child | Throttled; updates overall progress bar |
complete (partial) |
progress |
child | Snapshots child summary; waits for siblings |
complete (all done) |
complete |
parent | Terminal β all children finished |
error (all retries exhausted) |
error |
parent | Terminal β partial results in summary |
stop / pause |
stop / pause |
parent | Emitted once every child reaches the state |
Retries are handled silently inside
SyncLeaf. Noerrorevent surfaces during retry attempts β only on final failure.
Completion rule: a SyncComposite is complete when every child has reached a terminal state (complete, error, or idle β never syncing). If any child is in error the parent emits SyncStatus.error; otherwise SyncStatus.complete.
Getting Started #
1 β Define your leaf #
class UserProfileSync extends SyncLeaf<List<Map<String, dynamic>>> {
final Stream<List<Map<String, dynamic>>> stream;
StreamSubscription? _sub;
UserProfileSync(this.stream) : super(key: 'user_profile');
@override
int getTotalCount(data) => data.length;
@override
Future<void> start() async {
await super.start(); // β οΈ always call super β initializes lifecycle state
_sub = stream.listen((snapshot) => triggerSync(snapshot));
}
@override
Future<void> stop() async {
await _sub?.cancel();
_sub = null;
await super.stop();
}
@override
Future<void> performSync(data, onSyncOper) async {
for (final item in data) {
if (item['isUpToDate'] == true) {
await onSyncOper(SyncSummary.latest); // no-op at data layer
} else {
await onSyncOper(SyncSummary.update); // write to local DB
}
}
}
}
2 β Compose the tree #
final root = SyncComposite(
key: 'root',
primarySyncs: [
UserProfileSync(userStream),
SettingsSync(settingsStream),
],
lateSyncs: [
LogHistorySync(logStream),
],
stopOnError: false, // continue siblings on error; collect partial results
);
await root.start();
3 β React to state #
SyncState is a sealed class β the compiler enforces exhaustive handling:
final label = switch (state) {
SyncInitial() => 'Ready',
SyncInProgress(:final origin) => '${origin.key}: ${(origin.progress * 100).toStringAsFixed(1)}%',
SyncSuccess() => 'Done β',
SyncFailure(:final message) => 'Error: $message',
SyncPaused() => 'Paused',
SyncStopped() => 'Stopped',
};
Configuration #
ThrottlerConfig #
Controls how often progress updates reach the UI.
const ThrottlerConfig(
threshold: 0.01, // min progress delta to emit (1%)
interval: Duration(milliseconds: 100), // min time between emissions
precision: 1e-4, // float comparison tolerance
)
Use the built-in presets on Throttler for display-optimised rates:
Throttler.fps60(onUpdate: ...) // 16ms interval, 0.5% threshold
Throttler.fps30(onUpdate: ...) // 33ms interval, 1.0% threshold
RetryConfig #
Controls retry behaviour and exponential backoff.
const RetryConfig(
maxTryCount: 3, // retries after initial failure
baseDelayMs: 1000, // first retry delay: 1s
multiplier: 2.0, // delay doubles each retry
timeout: Duration(seconds: 30), // per-attempt time limit
maxJitterMs: 1000, // jitter ceiling (thundering herd)
)
Backoff formula: delay = baseDelayMs Γ multiplier^(nβ1) + jitter
Weighted Progress Formula #
Ξ£ completedCount (across all leaf nodes)
Total Progress = ββββββββββββββββββββββββββββββββββββββββββ
Ξ£ totalCount (across all leaf nodes)
A 1,000-item leaf alongside a 1-item leaf: the large leaf drives 99.9% of the bar β exactly as users expect.
License #
MIT β see LICENSE.
Author #
Jack (friend-22) Β· jack.leecnet@gmail.com Β· github.com/friend-22/flutter-sync-tree
Architecture review and code refinement assisted by Claude (Anthropic).

