# Flutter Integration

Plugin Kit's core package is pure Dart, with no `flutter` SDK dependency and no widget imports, so the same runtime drops into a CLI, a server, or a Flutter app unchanged. This page is about wiring that runtime into a widget tree, either directly or through whatever state library you already use.
**This page vs. Migrating a Flutter App:** **This page is the mechanics.** Where the runtime lives in a `StatefulWidget`, how widgets hear about plugin changes, how plugin-provided UI gets resolved. Use it when you are writing the widget-side bridge.

**[Migrating a Flutter App](https://plugin-kit.saad-ardati.dev/guides/migrating-flutter-app/) is the strategy.** Picking seams, deciding between direct service access and event-driven integration, and fitting Plugin Kit beside an existing Provider / Riverpod / Bloc / GetIt app without rewriting it. Use it when you already have an app and need to figure out where Plugin Kit should touch it.

They are meant to be read together. Most readers want the strategy page first, then come back here for the widget code.

The reference integration in this repo is [`example/code_editor/`](https://github.com/SaadArdati/plugin_kit/tree/main/example/code_editor) — try it at [plugin-kit.saad-ardati.dev/code-editor](https://plugin-kit.saad-ardati.dev/code-editor) — a full Flutter app built entirely on `StatefulWidget` and `setState` with no DI or state-management package. Everything below mirrors that pattern.
**Manual or with flutter_plugin_kit:** This page is the manual recipe: the shell owns the runtime, the session, the subscriptions, and the disposal calls itself. It is the most explicit version of the integration and the path the reference example takes.

For most apps, [`flutter_plugin_kit`](https://plugin-kit.saad-ardati.dev/guides/flutter-plugin-kit/) is cheaper. It ships an opt-in scope widget that constructs and disposes the runtime for you, a `State` mixin that auto-cancels subscriptions, and `BuildContext.watchEvent<E>()` for one-line event reads. It pulls in only `flutter` and `plugin_kit`, and exposes standard `ChangeNotifier` / `ValueListenable` shapes that drop into Provider, Riverpod, Bloc, and signals as ordinary values.

Read this page if you want the explicit recipe (or are migrating an app that already has its own shell). Read the [flutter_plugin_kit guide](https://plugin-kit.saad-ardati.dev/guides/flutter-plugin-kit/) if you want the boilerplate gone. Both packages are in the same monorepo and stay compatible release-to-release.

## The mental model

Plugin Kit can sit alongside your state management or carry a fair amount of state-management work itself. The bus, registry, and session lifecycle overlap with what most state libraries do, so the choice is about how much overlap you want to pay for, not about which tool is allowed to do which job.

| Tool | Good at |
|---|---|
| `Provider` / `Riverpod` / `Bloc` / `GetIt` | exposing values to widgets, scheduling rebuilds, mature widget integrations |
| Plugin Kit | lifecycle-aware feature composition, replaceable services, isolated sessions, runtime configuration, and (incidentally) a typed event bus that can drive `setState` directly |

If you keep both, the usual split is: state management exposes values to widgets, Plugin Kit decides which features exist and how they participate, and a small bridge connects the two. If you drop the state library, the session bus drives `setState` straight from the shell. The reference example takes that path.

For plugin-provided UI, that bridge often resolves widget factories from the
registry. For app behavior, prefer event contracts: widgets and state holders
emit typed events, plugins react, and the Flutter layer does not need to know
which service did the work.

The nicest Flutter surfaces for Plugin Kit are the ones users can see:

- a model selector whose options follow enabled plugins
- a customization dialog that edits runtime settings live
- a chat timeline that grows richer blocks as plugins stream progress and results

Those are all the same integration story. The widget tree provides the shell.
The runtime decides which features participate.

## The reference pattern

The cleanest integration, used by the `code_editor` example, looks like this.

1. A stateful shell widget holds a `PluginRuntime` and the current `PluginSession` as fields.
2. The shell subscribes to session-bus events it cares about and calls `setState` in those handlers.
3. Plugins contribute widgets to the shell by registering factories in the registry.
4. Plugins contribute descriptors (what should appear in the toolbar, panel list, status bar) by mutating collection events the shell emits.
5. When the user toggles a feature, the shell calls `updateSessionSettings(...)` with serialization to prevent races.
6. The shell's `dispose()` disposes the runtime, which tears down sessions cleanly.

No extra package. Just Flutter, with the shell owning every dispose call. (For a version that hides the bookkeeping behind a scope widget and a mixin, see [`flutter_plugin_kit`](https://plugin-kit.saad-ardati.dev/guides/flutter-plugin-kit/).)

## A minimal editor shell

```dart
extractRegion(flutterIntegrationSnippets, 'flutter-editor-shell-state')
```

That is the whole bridge. Session events trigger `setState`. Plugins contribute via the registry and via events. The widget tree never imports the `ServiceRegistry` or subscribes directly to plugin internals.

## Plugins contributing widgets

Widgets that plugins contribute live in the registry, behind an interface the shell knows about.

Define an interface in the shell:

```dart
extractRegion(codeEditorFactoriesSource, 'factories-panel-widget-factory')
```

Implement it in the plugin. The factory typically extends `StatefulPluginService` so it inherits session lifecycle and automatic subscription tracking.

```dart
extractRegion(flutterIntegrationSnippets, 'flutter-terminal-plugin')
```

The factory keeps its own state (`_history`). To trigger a rebuild, it emits `UIRefreshRequest` on the session bus. The shell hears it, calls `setState`, and the widget's `build(context)` runs as part of that rebuild.

Because the factory is a `StatefulPluginService`, the runtime manages its lifecycle automatically. It attaches when the plugin enables, detaches when the plugin disables, and its tracked subscriptions are cancelled without manual cleanup.

## Plugins contributing descriptors

For things like toolbar items, status bar entries, or panel lists, the shell emits a "collection" event. Plugins listen for it and append to the mutable list inside.

```dart
extractRegion(codeEditorContributionsSource, 'contributions-collect-panels')
```

Because every enabled plugin's handler runs in priority order on the same mutable event object, one emit produces the final list in-place. No separate collect-then-fold step.

## Toggling plugins at runtime

When a user enables or disables a plugin mid-session, the shell calls `updateSessionSettings(...)` on the runtime.

```dart
extractRegion(sessionsSnippets, 'session-update-settings')
```
**Serialize back-to-back toggles:** `updateSessionSettings` is async. If the user toggles two plugins in quick succession, a second reconciliation can start before the first finishes, and `PluginRuntime.updateSessionSettings` throws `StateError`.

The `code_editor` example serializes with a tail-chained `Future`:

```dart
extractRegion(flutterIntegrationSnippets, 'flutter-toggle-pending-serialize')
```

If you expose toggles to the user, serialize updates like this (or equivalent).

## Lifecycle gotchas

Dispose each session exactly once. `PluginRuntime.dispose()` iterates the active sessions it still owns and disposes them. If you plan to call `runtime.dispose()`, do not also call `session.dispose()` for those same sessions.

**`register` and `attach` do not re-run on hot reload.** Plugins are constructed once, and hot reload does not re-run `main` or re-execute `initState`. If you change code inside a plugin's `register` or `attach`, use hot restart. Pure widget code inside factories still hot-reloads fine, because that code runs on rebuild.

**Use `mounted` checks after async work.** Session event handlers are async. If a handler awaits something and then calls `setState`, check `mounted` first. The widget may have been disposed in the meantime.

## If you already use Riverpod, Provider, or Bloc

The shape of the integration above is the same. Hold the runtime where your package expects long-lived services, and expose the current session (or services resolved from it) through whatever mechanism you already use.

For the library-specific bridge code (Provider/`ChangeNotifier`, Riverpod
`AsyncNotifier`, flutter_bloc Cubit, signals_flutter, MobX, GetIt) see
[State Management Bridges](https://plugin-kit.saad-ardati.dev/reference/state-management-bridges/). Every
recipe there is implemented in `example/state_garden/` and run by
`flutter test`, so the citations are checked code rather than prose.

<Aside type="note" title="Don't bind the runtime's identity to widget rebuilds">
Resist the urge to stuff `PluginRuntime` into an `InheritedWidget` or a Provider that rebuilds the runtime whenever the tree rebuilds. The runtime outlives the widget tree (hot restart, deep navigation, route stack resets all throw away widgets but not the runtime).

Hold it somewhere whose lifetime matches the runtime's. The safest options are a top-level `final` or a GetIt singleton: neither depends on the widget tree. A Riverpod `Provider` at the root `ProviderScope` works too, but only when it is a non-`autoDispose` top-level provider, since `autoDispose` reconstructs the value once consumers drop. Inside the widget tree, `flutter_plugin_kit`'s `PluginRuntimeScope` is the right shape when the scope's lifetime really does match the runtime's: it constructs in `initState` and disposes in `dispose` without rebuilding.
</Aside>

## Testing Flutter widgets that use Plugin Kit

Plugin Kit works with `flutter_test` like any other Dart dependency. The pattern:

```dart
extractRegion(flutterIntegrationSnippets, 'flutter-integration-test-widgets')
```

For widgets that resolve services on build, register stubs or fakes in a test-only plugin. The registry does not distinguish "real" from "fake" implementations. Higher priority wins, same as production.

```dart
extractRegion(flutterIntegrationSnippets, 'flutter-fake-search-plugin')
```

See [Testing](https://plugin-kit.saad-ardati.dev/guides/testing/) for non-widget plugin tests.

## Related reading

[flutter_plugin_kit](https://plugin-kit.saad-ardati.dev/guides/flutter-plugin-kit/)
  [Custom Contexts](https://plugin-kit.saad-ardati.dev/concepts/custom-context/)
  [Runtime](https://plugin-kit.saad-ardati.dev/concepts/runtime/)
  [Settings & Overrides](https://plugin-kit.saad-ardati.dev/guides/settings/)
  [Testing](https://plugin-kit.saad-ardati.dev/guides/testing/)