Skip to content

v2.1.0 - Two-Phase Initialization for Reactive Services

Latest

Choose a tag to compare

@pillowsoft pillowsoft released this 03 Jan 20:23
· 4 commits to main since this release

Flutter PS App Shell v2.1.0

🎯 Two-Phase Initialization for Reactive Services

This release adds the onEffectsActivate callback to solve SignalEffectException in apps with reactive database services.

✨ Features

  • Added AppConfig.onEffectsActivate callback
    • Enables two-phase initialization for reactive services
    • Called AFTER appReady check, BEFORE rendering AppShell
    • Prevents SignalEffectException from dynamic signal creation
    • See docs/signals-best-practices.md for comprehensive guide

πŸ› Bug Fixes

  • Fixed SignalEffectException with reactive database services
    • Root cause: Dynamic signal creation inside effects (e.g., watchWhere() calls)
    • Solution: Two-phase pattern separates signal creation from effect creation
    • Services create watchers in initialize(), effects in activateEffects()
    • Framework calls onEffectsActivate() at correct time in lifecycle

πŸ“š Documentation

  • Added comprehensive Two-Phase Initialization section to docs/signals-best-practices.md
  • Created complete example service in example/lib/examples/two_phase_service_example.dart
  • Updated CLAUDE.md with two-phase pattern reference
  • Migration guide showing old vs new patterns

πŸ”„ Breaking Changes

None - onEffectsActivate is completely optional and backward compatible.

Apps without the callback continue working as before. Only apps with reactive services creating database watchers inside effects need to adopt the two-phase pattern.

πŸ“¦ Migration

If you have reactive services creating database watchers inside effects:

// ❌ OLD PATTERN - Dynamic signal creation inside effect
class MyService {
  Future<void> initialize() async {
    _effectCleanup = effect(() {
      final id = activeId.value;
      final watcher = db.watchWhere('items', {'userId': id});  // BAD!
      final items = watcher.value;
      untracked(() => itemsList.value = items);
    });
  }
}

// βœ… NEW PATTERN - Two-phase initialization
class MyService {
  late final ReadonlySignal<List<Map>> _itemsWatcher;

  Future<void> initialize() async {
    // Phase 1: Create watcher ONCE
    _itemsWatcher = db.watchWhere('items', {});
    final initial = await db.findWhere('items', {});
    itemsList.value = initial;
  }

  void activateEffects() {
    // Phase 2: Create effect from pre-existing watcher
    _effectCleanup = effect(() {
      final items = _itemsWatcher.value;
      untracked(() => itemsList.value = items);
    });
  }
}

// App usage
runShellApp(() async {
  final service = MyService();
  await service.initialize();

  return AppConfig(
    onEffectsActivate: () => service.activateEffects(),  // NEW
    routes: [/* ... */],
  );
});

πŸ” Why This Fix Works

The two-phase pattern solves the dynamic signal creation problem:

  • v2.0.6 solved: Write-side cycles (wrapping writes in untracked()) βœ…
  • v2.0.6 didn't solve: Read-side cycles from dynamic signal creation ❌
  • v2.1.0 solves: Both issues with two-phase initialization βœ…

Key insight: Creating signals inside effects (even with proper untracked()) causes reactive cycles when Watch widgets are active. The solution is to create all signals ONCE during initialization, then create effects that read from those pre-existing signals.

πŸ“‹ What About appReady?

The appReady signal from v2.0.5 is still useful for coordinating complex initialization timing. However, it alone doesn't prevent SignalEffectException - you need BOTH:

  1. appReady signal for initialization coordination (optional)
  2. Two-phase pattern for reactive services (required if using database watchers)

See docs/signals-best-practices.md for complete details and examples.

πŸ§ͺ Testing

  • 29 tests passed βœ…
  • 3 tests skipped (database tests requiring InstantDB setup - expected)
  • Backward compatible - apps without onEffectsActivate continue working

πŸ“– Resources

  • Documentation: docs/signals-best-practices.md
  • Example Service: example/lib/examples/two_phase_service_example.dart
  • CHANGELOG: Full details in packages/flutter_app_shell/CHANGELOG.md

Full Changelog: v2.0.6...v2.1.0