Setup
This content is for the 0.1 version. Switch to the latest version for up-to-date documentation.
The bindings are three things: a provider that scopes a runtime, a mixin that ties channel leases to widget lifetimes, and a selector that narrows rebuilds. Nothing else — no state-management package to adopt alongside the protocol.
flutter pub add ahp_flutterA complete app
Section titled “A complete app”This is example/lib/main.dart from the ahp_flutter repository. It runs
against a live host:
code agent host --port 51234 --without-connection-tokenflutter run -d macosimport 'package:ahp_flutter/ahp_flutter.dart';import 'package:flutter/material.dart';
const _hostUrl = String.fromEnvironment( 'AHP_URL', defaultValue: 'ws://localhost:51234',);
final _root = Uri.parse('ahp-root://');
void main() => runApp(const AhpExampleApp());
class AhpExampleApp extends StatefulWidget { const AhpExampleApp({super.key});
@override State<AhpExampleApp> createState() => _AhpExampleAppState();}
class _AhpExampleAppState extends State<AhpExampleApp> { late final AhpRuntime _runtime;
@override void initState() { super.initState(); _runtime = AhpRuntime( // A factory, not a single client: it is called on every attempt, so a // reconnect an hour later can dial a fresh URL or a refreshed token. connect: () async => AhpConnection( await WebSocketAhpTransport.connect(Uri.parse(_hostUrl)), ), clientId: 'ahp-flutter-example', ); }
@override void dispose() { _runtime.dispose(); super.dispose(); }
@override Widget build(BuildContext context) => AhpProvider( runtime: _runtime, child: MaterialApp( title: 'AHP', theme: ThemeData.dark(useMaterial3: true), home: const AgentList(), ), );}
class AgentList extends StatefulWidget { const AgentList({super.key});
@override State<AgentList> createState() => _AgentListState();}
class _AgentListState extends State<AgentList> with AhpChannelMixin { @override Widget build(BuildContext context) { final root = ahpWatch<RootState>(_root);
return Scaffold( appBar: AppBar(title: const Text('Agents')), body: ValueListenableBuilder<RootState>( valueListenable: root, builder: (context, state, _) => ListView.builder( itemCount: state.agents.length, itemBuilder: (context, i) { final agent = state.agents[i]; return ListTile( title: Text(agent.displayName), subtitle: Text(agent.provider), ); }, ), ), ); }}Three things are load-bearing there.
The runtime is owned by a State, not created in build. Building one per
frame would open a connection per frame.
AhpProvider wraps MaterialApp, not the other way around. Anything that
needs the runtime has to be below it.
ahpWatch comes from AhpChannelMixin. It acquires a lease the first time
and reuses it after, so calling it from build is safe — and the lease is
released when the State is disposed.
Connection state
Section titled “Connection state”AhpProvider exposes lifecycle separately from the runtime, so a subtree that
only dispatches does not rebuild on every connection transition:
class ConnectionBadge extends StatelessWidget { const ConnectionBadge({super.key});
@override Widget build(BuildContext context) { final connection = context.ahpConnection; return ValueListenableBuilder<AhpConnectionState>( valueListenable: connection, builder: (context, state, _) => switch (state.lifecycle) { AhpLifecycle.ready => const Icon(Icons.check_circle, size: 14), AhpLifecycle.catchingUp => const Text('Catching up…'), AhpLifecycle.awaitingAuth => const Text('Sign in required'), _ => const SizedBox.square( dimension: 12, child: CircularProgressIndicator(strokeWidth: 2), ), }, ); }}Two inherited layers rather than one is deliberate. A single scope carrying both the runtime and its lifecycle would rebuild every dependent on every transition — including subtrees that only ever needed the runtime to dispatch through.
Dispatching
Section titled “Dispatching”final handle = context.ahp.dispatch(channel, action);if (await handle.settled case DispatchRejected(:final reason)) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(reason))); }}context.ahp reaches the runtime without subscribing to lifecycle changes,
which is what you want on a button. See
Dispatching actions for the outcomes.
Two names are withheld
Section titled “Two names are withheld”The bindings re-export the whole SDK except Icon and TextSelection, which
would shadow Flutter’s. Reach them with a prefix:
import 'package:ahp_sdk/ahp_sdk.dart' as ahp;
final protocolIcon = ahp.Icon(/* ... */);- Scoping a runtime — what belongs where in the tree
- Narrowing rebuilds — selectors, and why coalescing comes first