Skip to content

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.

Terminal window
flutter pub add ahp_flutter

This is example/lib/main.dart from the ahp_flutter repository. It runs against a live host:

Terminal window
code agent host --port 51234 --without-connection-token
flutter run -d macos
import '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.

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.

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.

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(/* ... */);