Repository navigation
How should I use the Dart bridge when developing Flutter Service extensions? #6736
|
When integrating FletService, there is no overridden didChangeDependencies method and no available context. How can I implement this line of code: _frames = FletBackend.of(context).openDataChannel(); |
Replies: 2 comments 6 replies
|
A _frames = control.backend.openDataChannel();This works from a service's Important On the Python side, set Here's your service reworked. A few fixes beyond the channel line:
Dartimport 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flet/flet.dart';
import 'package:ultralytics_yolo/ultralytics_yolo.dart';
class YoloService extends FletService {
YoloService({required super.control});
DataChannel? _frames;
StreamSubscription<Uint8List>? _framesSub;
Future<YOLO>? _model;
@override
void init() {
super.init(); // keep init() synchronous: Flet doesn't await it
control.addInvokeMethodListener(_invokeMethod);
// No BuildContext needed: control.backend is the same FletBackend
// that FletBackend.of(context) returns.
final frames = _frames = control.backend.openDataChannel();
_framesSub = frames.messages.listen(_onFrame);
control.triggerEvent("data_channel_open", {
"channel_name": "frames",
"channel_id": frames.id,
});
_model = _loadModel()..ignore(); // load errors surface in _detect()
}
@override
void dispose() {
control.removeInvokeMethodListener(_invokeMethod);
_framesSub?.cancel();
_frames?.close();
unawaited(_model?.then((yolo) => yolo.dispose(), onError: (Object _) {}));
super.dispose();
}
Future<YOLO> _loadModel() async {
final src = control.getString("src");
if (src == null) throw ArgumentError("src is not set");
final yolo = YOLO(
// absolute path of a file in the Flet app's assets dir
modelPath: control.backend.getAssetSource(src).path,
task: YOLOTask.detect,
);
if (!await yolo.loadModel()) throw StateError("Failed to load '$src'");
return yolo;
}
Future<List<dynamic>> _detect(Uint8List image) async {
final yolo = await _model!; // waits while the model is still loading
final result = await yolo.predict(image);
return result["boxes"] as List? ?? const [];
}
// One-off images: `await yolo.detect_objects(jpeg)` in Python.
Future<dynamic> _invokeMethod(String name, dynamic args) async {
switch (name) {
case "detectObjects":
return _detect(args["image_bytes"] as Uint8List);
default:
throw Exception("Method $name is not implemented");
}
}
// Camera frames over the DataChannel.
// Python -> Dart: [request id: 4 bytes][JPEG/PNG]; Dart -> Python: [same id][JSON]
Future<void> _onFrame(Uint8List packet) async {
if (packet.length < 4) return;
final frame = Uint8List.fromList(packet); // own it: the buffer may be reused
Object reply;
try {
reply = {"boxes": await _detect(Uint8List.sublistView(frame, 4))};
} catch (e) {
reply = {"error": "$e"};
}
_frames?.send(Uint8List.fromList(
[...frame.take(4), ...utf8.encode(jsonEncode(reply))]));
}
}
class Extension extends FletExtension {
@override
FletService? createService(Control control) {
switch (control.type) {
case "YoloService": // must match @ft.control("YoloService") in Python
return YoloService(control: control);
default:
return null;
}
}
}Pythonimport asyncio
import json
from typing import Any, Optional
import flet as ft
@ft.control("YoloService") # must match `case "YoloService":` in Dart
class YoloService(ft.Service):
src: Optional[str] = None
on_data_channel_open: Optional[ft.EventHandler[ft.DataChannelOpenEvent]] = None
def init(self):
self._channel: Optional[ft.DataChannel] = None
self._channel_ready = asyncio.Event()
self._pending: dict[bytes, asyncio.Future] = {}
self._next_id = 0
# Set BEFORE super().init(): that's where the service is sent to Flutter,
# and Dart only fires data_channel_open if a handler was set by then.
if self.on_data_channel_open is None:
self.on_data_channel_open = lambda e: None
super().init()
def before_event(self, e):
# Grab the channel here, so it works even with a user-supplied handler.
if isinstance(e, ft.DataChannelOpenEvent):
self._loop = asyncio.get_running_loop()
self._channel = self.get_data_channel(e.channel_id)
self._channel.on_bytes(self._on_bytes)
self._channel_ready.set()
return super().before_event(e)
def _on_bytes(self, payload: bytes):
# can be called off the event-loop thread in built apps
self._loop.call_soon_threadsafe(self._resolve, payload[:4], payload[4:])
def _resolve(self, request_id: bytes, body: bytes):
fut = self._pending.pop(request_id, None)
if fut is not None and not fut.done():
fut.set_result(json.loads(body))
async def detect_frame(self, image: bytes, timeout: float = 30) -> list[dict[str, Any]]:
"""Camera frames over the DataChannel. Await each call before sending the next."""
await asyncio.wait_for(self._channel_ready.wait(), timeout)
self._next_id = (self._next_id + 1) & 0xFFFFFFFF
request_id = self._next_id.to_bytes(4, "little")
fut = self._pending[request_id] = asyncio.get_running_loop().create_future()
self._channel.send(request_id + image)
try:
reply = await asyncio.wait_for(fut, timeout)
finally:
self._pending.pop(request_id, None)
if "error" in reply:
raise RuntimeError(reply["error"])
return reply["boxes"]
async def detect_objects(self, image: bytes, timeout: float = 30) -> list[dict[str, Any]]:
"""One-off images over the regular protocol."""
return await self._invoke_method(
"detectObjects", {"image_bytes": image}, timeout=timeout
)Usage: yolo = YoloService(src="yolo11n.tflite") # file in your assets dir
page.services.append(yolo) # keeps a reference so the service isn't dropped
boxes = await yolo.detect_objects(jpeg_bytes) # one-off image
boxes = await yolo.detect_frame(jpeg_bytes) # camera stream, one frame at a timeA few notes:
Docs: https://flet.dev/docs/extend/user-extensions#dedicated-data-channels. The page only shows widgets right now; we'll add the service case. |


A
FletServicedoesn't need aBuildContext.FletBackend.of(context)is only a Provider lookup, and everyControlalready has a reference to that same backend:This works from a service's
init(), and from anywhere else you have acontrol.Important
On the Python side, set
on_data_channel_openbeforesuper().init().In Dart,
triggerEvent("data_channel_open", ...)is only sent if Python registered a handler. AServiceis sent to Flutter from insideService.init(), so a handler set aftersuper().init()never reaches Flutter, and the event is silently dropped. Passing the handler to the constructor also works.I am working on improving this.
Here's …