Repository navigation
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.onEffectsActivatecallback- Enables two-phase initialization for reactive services
- Called AFTER
appReadycheck, BEFORE rendering AppShell - Prevents SignalEffectException from dynamic signal creation
- See
docs/signals-best-practices.mdfor 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 inactivateEffects() - Framework calls
onEffectsActivate()at correct time in lifecycle
- Root cause: Dynamic signal creation inside effects (e.g.,
π 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.mdwith 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:
appReadysignal for initialization coordination (optional)- 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
onEffectsActivatecontinue 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