The state_garden workshop
example/state_garden/ is a workshop. The same plugin_kit chat protocol (a ChatService, two events, two plugins) is bridged to ten Flutter state-management approaches side by side, with the exact same ChatView rendering every variant. You read across to see what each library costs and which bookkeeping it abstracts away.
This is the canonical source of truth for State Management Bridges. Every recipe on that reference page is implemented here, executed under flutter test, and kept clean by flutter analyze. If a future plugin_kit change quietly breaks one of the recipes, a test in this package fails.
Run it
Section titled “Run it”Try the live web build at plugin-kit.saad-ardati.dev/state-garden, or run locally from the workspace root:
flutter pub getflutter run --target example/state_garden/lib/main.dartThe app boots a runtime, wires the locators each integration expects, and renders an integration launcher. Pick any tile to swap in that library’s ChatScreen. Type a message; the bot replies echo: <your text>. The same protocol runs underneath all ten.
The ten integrations
Section titled “The ten integrations”All under lib/src/integrations/:
- setState (no library). The baseline against which every other recipe is compared.
flutter_plugin_kitPluginSessionStateListener. The State-mixin variant of setState with subscription bookkeeping abstracted away.plugin_kitPluginSessionListener. The same mixin pattern wired directly fromplugin_kit, no Flutter-side dependency.ChangeNotifier+provider. The “classic” Flutter shape.flutter_plugin_kitPluginEventNotifier. A foundationChangeNotifier/ValueListenablefor “the latest event of type T”, with no custom subclass to write.flutter_blocCubit. With a value-equalityChatBlocStatesoBlocBuilderskips identical snapshots.- Riverpod
AsyncNotifier. AProvider<PluginSession>overridden at app boot. signals_flutter. Reactive primitives consumed viaWatch((context) => ...).- MobX. No code generation; explicit
Observerwidgets. - GetIt as a session locator. Service locator for the session, plain
setStatefor UI.
Each integration owns one bridge class (or one screen, for the no-bridge variants) plus a screen widget. The ten screens render through a shared ChatView so the test harness can type into the same key, tap the same key, and assert against the same MessageList regardless of which bridge is under test.
Six lifecycle proofs
Section titled “Six lifecycle proofs”Beyond the integrations, test/lifecycle_proofs_test.dart holds six pure-plugin_kit tests with no widgets. They are written against the same ChatService the integrations resolve, so the proofs and the recipes can never drift.
- Settings reconcile. Disabling a plugin via
updateSessionSettingsremoves its event handlers but does not dispose the session bus or registry instances. - Session swap. Each session constructs its own service instances; an old session’s service is frozen after
session.dispose. - Two live sessions stay isolated. Messages emitted on one session never reach the other session’s resolved
ChatService. - Canonical dispose.
runtime.dispose()alone tears down session buses and drains the sessions list, with no need to callsession.dispose()separately. - Hot-swap. A higher-priority registrant wins resolution; disabling it via settings reconciliation flips the winner without touching the session.
- Toggle guard. Two
updateSessionSettingscalls fired concurrently withFuture.waitthrowStateError; tail-chained serialization converges on the latest intent. Empirical proof of the runtime guard and serialization pattern called out in the state management research note.
How to read the code
Section titled “How to read the code”Start at lib/state_garden.dart for the public API surface. Then:
lib/src/chat/holds the chat protocol:ChatMessage, two events,ChatServiceandAltChatService, and the two plugins that register them.lib/src/widgets/holds the shared UI:MessageList,MessageInput, andChatViewthat composes them. No widget-returning helper methods anywhere.lib/src/integrations/holds one file per library, each documenting why the bridge is shaped that way.lib/src/runtime_holder.dartis the test fixture and example boot path.lib/main.dartboots the runtime, wires the locators each integration expects, and renders the launcher.
Architecture rules applied
Section titled “Architecture rules applied”ChatMessageandChatBlocStatesupport value equality so observers do not rebuild on identical snapshots.- Every async continuation that touches widget or holder state guards with
mounted,isClosed, or a local_disposedflag. - Every visual chunk is a real
StatelessWidgetorStatefulWidgetclass. No widget-returning helper methods. - Bridges depend on the abstract
PluginSessiontype; nothing reaches past it into concrete plugin internals.