flutter_sync_tree 1.0.9 copy "flutter_sync_tree: ^1.0.9" to clipboard
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.

Pub Version Pub Points Pub Likes Live Demo


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. No error event 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).

2
likes
160
points
304
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A high-performance, hierarchical synchronization framework with weighted progress and throttling for Flutter.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

collection, equatable, flutter

More

Packages that depend on flutter_sync_tree