Skip to content

Scoping a runtime

ahp_flutter is built on InheritedWidget, InheritedNotifier, and ValueNotifier. There is no state-management package to adopt alongside the protocol.

Terminal window
flutter pub add ahp_flutter

It re-exports the whole client, so one import is enough.

AhpProvider(
runtime: AhpRuntime(
connect: () async => AhpConnection(
await WebSocketAhpTransport.connect(Uri.parse('ws://localhost:51234')),
),
clientId: 'my-app',
),
child: const MyApp(),
);

The provider installs two inherited layers, and the split is the point.

context.ahp returns the runtime and does not subscribe to rebuilds. The runtime is identity-stable for the life of the tree, so depending on it would cost an element registration and buy a rebuild that can never fire.

context.ahpConnection returns the lifecycle and does rebuild, on every transition.

final state = context.ahpConnection;
if (state.phase == AhpLifecycle.reconnecting) {
return Text('reconnecting (attempt ${state.attempt})');
}

A single scope carrying both would rebuild every dependent on every connection transition — including subtrees that only ever needed the runtime to dispatch through. A widget test pins the distinction: the lifecycle watcher rebuilds while the runtime-only watcher does not.

AhpChannel<RootState>(
channel: Uri.parse('ahp-root://'),
builder: (context, root, _) => Text('${root.agents.length} agents'),
);

For a State that needs several channels, use the mixin:

class _MyPageState extends State<MyPage> with AhpChannelMixin<MyPage> {
@override
Widget build(BuildContext context) => ValueListenableBuilder(
valueListenable: ahpWatch<ChatState>(widget.chatUri),
builder: (context, chat, _) => …,
);
}

Two widgets watching the same channel share one wire subscription and one store. Release is idempotent, and the last release starts a grace window before teardown, so a push-and-pop does not churn the wire.

Widget tests can drive a real AhpRuntime against a fake AhpClient. Two settings keep its timers from outliving the tree and tripping flutter_test’s pending-timer assertion:

AhpRuntime(
connect: () async => fakeClient,
clientId: 'test',
keepaliveInterval: null, // no periodic ping
lingerDuration: Duration.zero, // tear subscriptions down inline
);