How Wonderous manages application state using GetIt dependency injection and Provider-based reactivity
Wonderous uses a combination of GetIt for dependency injection and Provider (via GetItMixin) for reactive state management. This approach provides clean separation between business logic and UI while maintaining reactive updates.
For convenience, logic controllers (not services) have global getters in main.dart:101-110:
// Logic controllers - encouraged for use in UIAppLogic get appLogic => GetIt.I.get<AppLogic>();WondersLogic get wondersLogic => GetIt.I.get<WondersLogic>();TimelineLogic get timelineLogic => GetIt.I.get<TimelineLogic>();SettingsLogic get settingsLogic => GetIt.I.get<SettingsLogic>();UnsplashLogic get unsplashLogic => GetIt.I.get<UnsplashLogic>();ArtifactAPILogic get artifactLogic => GetIt.I.get<ArtifactAPILogic>();CollectiblesLogic get collectiblesLogic => GetIt.I.get<CollectiblesLogic>();LocaleLogic get localeLogic => GetIt.I.get<LocaleLogic>();
Services like ArtifactAPIService deliberately do not have global accessors. This encourages using logic controllers as the interface layer, keeping services isolated.
Logic classes expose state as ValueNotifier properties. From SettingsLogic in lib/logic/settings_logic.dart:5-13:
class SettingsLogic with ThrottledSaveLoadMixin { late final hasCompletedOnboarding = ValueNotifier<bool>(false) ..addListener(scheduleSave); late final hasDismissedSearchMessage = ValueNotifier<bool>(false) ..addListener(scheduleSave); late final isSearchPanelOpen = ValueNotifier<bool>(true) ..addListener(scheduleSave); late final currentLocale = ValueNotifier<String?>(null) ..addListener(scheduleSave);}
Key Points:
Each piece of state is a separate ValueNotifier
Listeners can be attached for side effects (like auto-saving)
Logic controllers follow a consistent pattern. Example from CollectiblesLogic:
class CollectiblesLogic with ThrottledSaveLoadMixin { @override String get fileName => 'collectibles.dat'; // Data final List<CollectibleData> all = collectiblesData; // Reactive State late final statesById = ValueNotifier<Map<String, int>>({}) ..addListener(_updateCounts); // Derived State (computed from reactive state) int _discoveredCount = 0; int get discoveredCount => _discoveredCount; // Service Dependencies late final _nativeWidget = GetIt.I<NativeWidgetService>(); // Initialization void init() => _nativeWidget.init(); // Business Logic Methods void setState(String id, int state) { Map<String, int> states = Map.of(statesById.value); states[id] = state; statesById.value = states; // Triggers listeners scheduleSave(); } // Private Helpers void _updateCounts() { // Update derived state when reactive state changes }}
// Update ValueNotifier to trigger UI rebuildvoid updateValue() { myValue.value = newValue; // All watchers rebuild}
Service Access from Logic
class MyLogic { // Access other services via GetIt late final _apiService = GetIt.I<ArtifactAPIService>(); Future<void> doWork() async { final data = await _apiService.fetchData(); // Process data... }}
Initialization and Bootstrap
class AppLogic { Future<void> bootstrap() async { // Load settings await settingsLogic.load(); // Initialize data wondersLogic.init(); timelineLogic.init(); // Load user data await collectiblesLogic.load(); }}
class ArtifactAPIService { // Not exposed globally, accessed via GetIt inside logic Future<List<Artifact>> search(String query) async { // API call final response = await http.get(url); return parseArtifacts(response); }}class ArtifactAPILogic { // Logic class wraps service late final _service = GetIt.I<ArtifactAPIService>(); // Exposed to UI Future<void> searchArtifacts(String query) async { final results = await _service.search(query); // Process results, update state }}
Compute values from reactive state without storing redundantly:
class CollectiblesLogic { late final statesById = ValueNotifier<Map<String, int>>({})..addListener(_updateCounts); // Derived state - computed from statesById int _discoveredCount = 0; int get discoveredCount => _discoveredCount; void _updateCounts() { _discoveredCount = 0; statesById.value.forEach((_, state) { if (state == CollectibleState.discovered) _discoveredCount++; }); }}