Skip to content

FAQ

This page answers the recurring “why does it work this way” questions. Symptom-driven entries (something is broken, fix it) live on the Troubleshooting page.

If your app has one HTTP client, one auth service, one analytics service, and a few screens that call them, use the boring thing. Plugin Kit earns its weight when behavior needs to be replaced, layered, disabled, overridden, or vetoed while the app is running, and settings have stopped being data your app reads and started being something that actively reshapes the system. If you do not have those needs yet, adding Plugin Kit is overhead with no payoff.

Should I use Plugin Kit, Provider, Riverpod, Bloc, or GetIt?

Section titled “Should I use Plugin Kit, Provider, Riverpod, Bloc, or GetIt?”

State management owns presentation state. Plugin Kit owns participation. They answer different questions.

  • A chat screen showing messages is presentation state.
  • A plugin deciding whether it wants to enrich an outgoing prompt is participation.
  • A model selector showing the current model is presentation state.
  • The list of available models changing because provider plugins were enabled or disabled is participation.
  • A loading spinner is presentation state.
  • A fallback provider taking over because the first one is unavailable is participation.

Most apps keep both. Provider, Riverpod, Bloc, and GetIt cover the DI / widget-rebuild space at the app layer with mature widget integrations. Plugin Kit owns the runtime protocol underneath: priority-resolved services, typed event coordination, settings-driven plugin enable/disable. The registry doubles as DI; the bus doubles as a state-update channel. Some apps drop the state library and let the bus do that work; the reference code_editor example takes that path with plain setState. Either is fine.

The Migrating a Flutter App guide covers the canonical bridges (Provider/ChangeNotifier, Riverpod notifiers that surface bus events as AsyncValue, Cubits that translate user intents into emit() calls), and flags where those bridges turn into pure forwarding. If you have an existing app, that page is the right starting point.

Constant time after the first resolution. Singletons cache their instance the moment the registry constructs them; lazy singletons cache on first resolve. Factory registrations call the constructor every time, which is the explicit contract of a factory. Priority-sorted insertion happens at registration time, not at resolve, so resolution is a single map lookup plus an array head read.

If you are calling resolve in a hot path and worried about it, the library is not the bottleneck. Profile before optimizing.

Each session is its own scope. PluginRuntime holds the global scope; PluginSession is created per session and disposed at session end. Within a session, the bus is single-threaded: handlers run sequentially in the order the cascade dictates.

updateSessionSettings and updateGlobalSettings are async. Inside a single call, the runtime reconciles plugins one at a time. Across two concurrent calls, you are responsible for serialization (see the back-to-back toggles entry in Troubleshooting).

There is no implicit isolate boundary. If you need genuine parallelism, spawn isolates from inside a plugin and bridge results back through the bus.

Why is the dialog package architecturally split?

Section titled “Why is the dialog package architecturally split?”

plugin_kit is dart-only. It carries the declaration types, including UiConfigurableCapability, ConfigField, and the rest of the dialog’s data model. Your non-Flutter packages can declare what their services configure without taking a Flutter dependency.

plugin_kit_dialog is the Flutter UI on top. It reads the dialog declarations and renders them, and it lives in its own package so server-side and CLI consumers of plugin_kit never have to pull in Flutter.

The split is what lets a backend Dart package describe its UiConfigurableCapability once, then have any Flutter app surface a real settings UI for it without the backend ever knowing Flutter exists.

Yes, through the global scope. Use a GlobalPlugin, register services there, and emit on globalBus when you want every session to react. Cross-session broadcast also has the SessionBroadcast extension on runtime.sessions, which iterates active sessions and emits on each session bus.

What you should not do is pass session-scoped service instances directly between sessions. They are not thread-safe across the boundary, their lifecycle is tied to a specific session, and their event subscriptions are scoped to one session bus. If you find yourself wanting to do this, you wanted a global service.

The Reference section of these docs is the curated reference: every public type grouped by intent (plugins, registry, bus, settings, dialog), with the canonical signatures and the common usage shapes inline. It is curated, not exhaustive. When a method has three overloads and two of them are rarely the right call, the reference says so.

Auto-generated dartdoc will live on pub.dev once the packages ship there. It will be exhaustive and organized the way the source is. Both will coexist. Each curated reference page will eventually link to the relevant pub.dev section.

Why two pages on events (event-bus and events in concepts)?

Section titled “Why two pages on events (event-bus and events in concepts)?”

/concepts/event-bus/ is the model: priorities, dispatch order, identifier scoping, request/response, the global tap, error propagation. It is the page you read once and refer back to.

/concepts/events/ is the patterns: how plugins use the bus from the inside, how envelopes flow, the canonical “do X on Y” shapes, and the common pitfalls list (unwrapped envelopes, forgetting the context arg on Plugin helpers, emitting from constructors). It is the page you read while writing your first plugin.

Both link to each other. Splitting them keeps the model page short and the patterns page concrete.

What happens to a lazy_singleton service if no one ever resolves it?

Section titled “What happens to a lazy_singleton service if no one ever resolves it?”

It is never constructed. The factory you passed to registerLazySingleton is held until the first resolve<T> call, then run once, then the result is cached for every subsequent resolve. If nothing ever asks, the constructor never runs.

This is by design. Lazy singletons are how you express “this service is expensive, only build it if someone needs it.” If you want eager construction, use registerSingleton.