Xybrid
SDKs

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 = 28 or higher in defaultConfig of android/app/build.gradle.kts. Flutter's default, flutter.minSdkVersion, is lower.
  • iOS: set platform :ios, '16.0' in ios/Podfile and the Runner target's iOS Deployment Target to 16.0 in Xcode.
  • macOS: set platform :osx, '14.0' in macos/Podfile and the Runner target's macOS Deployment Target to 14.0 in Xcode.

Installation

Add to your pubspec.yaml:

dependencies:
  xybrid_flutter: ^0.16.0

Initialization

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:

  1. Create a loader — specify the model by registry ID or local bundle
  2. Load — download (if needed) and initialize the model
  3. 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:

EventPropertiesDescription
LoadProgressprogress (0.0–1.0), percentage (0–100), downloadedBytes, totalBytes (nullable)Download/load progress
LoadComplete—Model is ready
LoadErrormessageLoading 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

PlatformStatusAccelerators
macOSSupportedMetal, CoreML
iOSSupportedMetal, CoreML
AndroidSupportedNNAPI
LinuxSupportedCPU
WindowsPlannedDirectML

On this page