Flutter
On-device ML inference for Flutter applications
The Flutter SDK (xybrid_flutter) provides Dart bindings to the Xybrid Rust core via FFI, enabling on-device ML inference in mobile and desktop applications.
Requirements
- iOS 16.0+
- macOS 14.0+ on Apple Silicon
- Android 9 / API 28+
The precompiled native libraries are built for these versions. For releases after 0.16.0 the plugin also declares them, so an app that targets an older version fails at build time. Raise your app's minimums:
- Android: set
minSdk = 28or higher indefaultConfigofandroid/app/build.gradle.kts. Flutter's default,flutter.minSdkVersion, is lower. - iOS: set
platform :ios, '16.0'inios/Podfileand the Runner target's iOS Deployment Target to 16.0 in Xcode. - macOS: set
platform :osx, '14.0'inmacos/Podfileand the Runner target's macOS Deployment Target to 14.0 in Xcode.
Installation
Add to your pubspec.yaml:
dependencies:
xybrid_flutter: ^0.16.0Initialization
Initialize the SDK before using any Xybrid APIs:
import 'package:xybrid_flutter/xybrid_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Xybrid.init();
runApp(MyApp());
}Inference runs entirely on-device whether or not you authenticate. Pass an apiKey to light up the dashboard — that single call starts the telemetry exporter, and your model.run() / pipeline.run() calls automatically emit execution traces:
await Xybrid.init(
apiKey: const String.fromEnvironment('XYBRID_API_KEY'),
);Without a key, telemetry is disabled and the first inference logs a one-shot hint pointing at the dashboard (suppress with the XYBRID_QUIET=1 environment variable). Get a free key at dashboard.xybrid.dev. For a self-hosted dashboard, also pass ingestUrl; for CPU/memory sampling, pass resourceTelemetry.
Xybrid.init() also subscribes to OS battery state on iOS and Android via battery_plus and forwards each reading to the routing engine.
Core Pattern: Load → Run
All inference follows the same pattern:
- Create a loader — specify the model by registry ID or local bundle
- Load — download (if needed) and initialize the model
- Run — execute inference with an input envelope
final model = await Xybrid.model('kokoro-82m').load();
final result = await model.run(XybridEnvelope.text('Hello world'));
print(result.text);Text-to-Speech
final model = await Xybrid.model('kokoro-82m').load();
final envelope = XybridEnvelope.text('Hello, how are you?', voiceId: 'af_heart');
final result = await model.run(envelope);
// Get WAV-wrapped audio bytes for playback
final wavBytes = result.audioAsWav(sampleRate: 24000);Speech-to-Text
final model = await Xybrid.model('whisper-tiny-ggml').load();
final envelope = XybridEnvelope.audio(bytes: audioBytes, sampleRate: 16000);
final result = await model.run(envelope);
print(result.text); // "Hello, how are you?"LLM Chat with Streaming
final model = await Xybrid.model('gemma-3-1b').load();
final context = ConversationContext();
context.setSystem('You are a helpful assistant.');
context.pushText('Tell me a joke', MessageRole.user);
final envelope = XybridEnvelope.text('Tell me a joke');
await for (final token in model.runStreamingWithContext(envelope, context)) {
stdout.write(token.token); // Print each token as it arrives
}Loading with Progress
final loader = XybridModelLoader.fromRegistry('kokoro-82m');
await for (final event in loader.loadWithProgress()) {
switch (event) {
case LoadProgress(:final percentage, :final downloadedBytes, :final totalBytes):
print('Loading: $percentage% ($downloadedBytes / ${totalBytes ?? '?'} bytes)');
case LoadComplete():
print('Model ready!');
case LoadError(:final message):
print('Error: $message');
}
}progress spans every file the model needs — weights plus companions such as a
vision projector — and never moves backwards, so the bar neither restarts per
file nor rewinds when a download retries. totalBytes is null when the source
publishes no size (a Hugging Face repo); downloadedBytes is exact either way,
which is what you need for megabytes, speed, and time remaining.
API Reference
Xybrid
Main entry point for the SDK.
class Xybrid {
static Future<void> init({
String? apiKey, // authenticates telemetry; omit to run anonymously
String? gatewayUrl, // override the LLM gateway URL
String? ingestUrl, // override the telemetry ingest URL (self-hosting)
String? resourceTelemetry, // enable CPU/memory sampling
});
static bool get isInitialized;
static void setApiKey(String apiKey);
static XybridModelLoader model(String modelId);
static XybridPipeline pipeline({String? yaml, String? filePath});
}XybridModelLoader
Creates and loads models from different sources.
class XybridModelLoader {
factory XybridModelLoader.fromRegistry(String modelId);
factory XybridModelLoader.fromBundle(String path);
Future<XybridModel> load();
Stream<LoadEvent> loadWithProgress();
}Load events:
| Event | Properties | Description |
|---|---|---|
LoadProgress | progress (0.0–1.0), percentage (0–100), downloadedBytes, totalBytes (nullable) | Download/load progress |
LoadComplete | — | Model is ready |
LoadError | message | Loading failed |
XybridModel
A loaded model ready for inference.
class XybridModel {
Future<XybridResult> run(XybridEnvelope envelope, {GenerationConfig? config});
Future<XybridResult> runWithContext(XybridEnvelope envelope, ConversationContext context, {GenerationConfig? config});
Stream<StreamToken> runStreaming(XybridEnvelope envelope, {GenerationConfig? config});
Stream<StreamToken> runStreamingWithContext(XybridEnvelope envelope, ConversationContext context, {GenerationConfig? config});
}XybridEnvelope
Input container for model inference.
class XybridEnvelope {
factory XybridEnvelope.audio({
required List<int> bytes,
required int sampleRate,
int channels = 1,
});
factory XybridEnvelope.text(String text, {String? voiceId, double? speed});
factory XybridEnvelope.embedding(List<double> data);
factory XybridEnvelope.textWithRole(String text, MessageRole role);
XybridEnvelope withRole(MessageRole role);
}XybridResult
Output from model inference.
class XybridResult {
bool get success;
String? get text; // ASR / LLM output
Uint8List? get audioBytes; // TTS output (raw PCM)
List<double>? get embedding; // Embedding output
int get latencyMs;
Uint8List? audioAsWav({int sampleRate = 24000, int channels = 1});
}StreamToken
Token from streaming LLM inference.
class StreamToken {
final String token; // Generated token text
final int index; // Token position
final String cumulativeText; // All text so far
final bool isFinal; // Last token in sequence
final String? finishReason; // "stop", "length", "error"
}ConversationContext
Multi-turn conversation management for LLM models.
class ConversationContext {
ConversationContext();
ConversationContext.withId(String id);
String get id;
int get historyLength;
bool get hasSystem;
void setSystem(String text);
void setMaxHistoryLength(int length);
void push(XybridEnvelope envelope);
void pushText(String text, MessageRole role);
void clear(); // Clears history, keeps system prompt
}enum MessageRole { system, user, assistant }XybridPipeline
Multi-stage pipeline for chaining models.
class XybridPipeline {
factory XybridPipeline.fromYaml(String yaml);
factory XybridPipeline.fromFile(String path);
String? get name;
int get stageCount;
List<String> get stageNames;
Future<XybridResult> run(XybridEnvelope envelope);
}Pipeline Example
final yaml = '''
name: voice-assistant
stages:
- whisper-tiny
- gemma-3-1b
- kokoro-82m
''';
final pipeline = XybridPipeline.fromYaml(yaml);
final result = await pipeline.run(
XybridEnvelope.audio(bytes: audioBytes, sampleRate: 16000),
);
// Result is TTS audio from the final stage
final wav = result.audioAsWav();XybridException
class XybridException implements Exception {
final String message;
}All SDK errors throw XybridException. Loading errors are surfaced via LoadError events when using loadWithProgress().
Platform Support
| Platform | Status | Accelerators |
|---|---|---|
| macOS | Supported | Metal, CoreML |
| iOS | Supported | Metal, CoreML |
| Android | Supported | NNAPI |
| Linux | Supported | CPU |
| Windows | Planned | DirectML |