Flutter Local AI
A Flutter package that provides a unified API for local AI inference on Android with ML Kit GenAI, on Apple Platforms using Foundation Models, and on Windows using Windows AI APIs (Windows AI Foundry).
Text generation (blocking or streamed), structured JSON outputs, tool calling, and generative UI: the on-device model can design small typed-block UI modules that render with the genui runtime — and, with tool calls, operate them afterwards.
#ai #genui #structured-outputs #on-device-ai #gemini-nano #foundation-models
✨ Unique Advantage
This package uses OS-managed models through native APIs, with no app-bundled model checkpoint.
- iOS: Uses Apple's built-in FoundationModels framework (iOS 26.0+) - system-managed preparation may be required
- Android: Uses Google's ML Kit GenAI (Gemini Nano) - leverages the native on-device model
- Windows: Uses Windows AI APIs (Windows AI Foundry) - the build resolves the Windows App SDK itself; running needs a Copilot+ PC or supported GPU and a packaged app
- No bundled checkpoints: The OS may download model assets during preparation
- Native Performance: Direct access to OS-optimized AI capabilities
- Smaller App Size: The OS manages model weights; the app still includes the plugin and SDK dependencies
- Structured Outputs: On Apple platforms, Windows and Chrome, constrain generation to a JSON Schema and read the decoded object with
AiResponse.json - Generative UI: Turn a natural-language goal into a renderable
genuimodule spec, on-device, identically on Apple FoundationModels and Android Gemini Nano (Pixel, Samsung, Xiaomi, OnePlus and more)
Platform Support
| Feature | iOS / macOS (26+) | Android (API 26+) | Windows (11 25H2+, Copilot+ / supported GPU) | Web (Chrome) |
|---|---|---|---|---|
| Text generation | ✅ | ✅ | ⚠️ unverified on device | ✅ |
| Streaming | ✅ | ✅ | ⚠️ single chunk | ✅ |
| Structured outputs | ✅ | ❌ ML Kit is compile-time only | ⚠️ native, unverified | ✅ |
| Image input | ⚠️ OS 27 SDK + runtime | ✅ | ❌ | ❌ |
| Generative UI (genUI) | ✅ | ✅ | 🚧 Planned | 🚧 Planned |
| Summarization* | 🚧 Planned | 🚧 Planned | 🚧 Planned | 🚧 Planned |
| Image generation | 🚧 Planned | ❌ | 🚧 Planned | ❌ |
| Tool calls | ✅ native | ❌ no ML Kit API | ❌ no Windows AI API | ❌ |
| Exact token counts | ✅ OS 26.4+ | ✅ | ❌ estimate | ✅ |
| Concurrent sessions | ✅ | ✅ | ✅ | ✅ |
*Summarization is achieved through text-generation prompts and shares the same API surface.
"Unverified" means the code compiles against the vendor SDK but has not yet run on a device that meets the vendor's hardware requirements; see known limitations and fallbacks for what each ❌ is blocked on, and platform support for the detail.
Every row is a runtime property, not a build-time one — the same binary reports image input as unavailable on iOS 26; an OS 27 SDK build can enable it on iOS 27. That new branch still needs Xcode 27/device validation. Ask the device rather than the platform:
final caps = await LocalAi.capabilities();
if (caps.supportsVision) { /* ... */ }
Two APIs
Two surfaces, one implementation: the prompt-oriented API is a facade over the session layer, not a second code path. Pick by what you need.
FlutterLocalAi — the original one-shot API. One process-wide session,
generateText / generateTextStream, native tool calling, schema-constrained
output, genUI specs. Unchanged and fully supported.
LocalAiModel / LocalAiSession — the session API. Several independent
conversations at once, a turn built from parts (addQueryChunk, addImage)
and then generated, real cancellation, exact token counts, and explicit
lifecycle. This is the surface that adapters can use to share the same
native implementation.
await LocalAi.ensureReady(onProgress: (p) => debugPrint('$p%'));
final model = await LocalAiModel.create(maxTokens: 4096);
final session = await model.openSession(systemInstruction: 'Be concise.');
await session.addQueryChunk('Summarize this in one line: ...');
await for (final chunk in session.getResponseAsync()) {
stdout.write(chunk);
}
await session.close();
await model.close();
Relationship to flutter_gemma
This is a standalone plugin: it depends on no flutter_gemma package, and
owns its native backends and Chrome Prompt API arm outright. The bridge that
lets flutter_gemma treat the OS model as one of its inference engines is
flutter_gemma_builtin_ai,
which lives in the flutter_gemma repository — so a flutter_gemma interface
change and the bridge that follows it ship together, in one upstream PR. The
dependency only ever points that way: the bridge may depend on this package,
this package never depends on flutter_gemma. If you already use that package,
nothing changes: it keeps its own name, imports and BuiltInAi* API. If you
only want the OS model, depend on this package alone and skip flutter_gemma's
plugin, downloader and SDK floor.
See platform support for what each backend can actually do, what it needs at build time, and what has been verified.
Installation
Add this to your package's pubspec.yaml file:
dependencies:
flutter_local_ai:
git:
url: https://github.com/kekko7072/flutter_local_ai.git
Or if published to pub.flutter-io.cn:
dependencies:
flutter_local_ai: latest
Android setup
Use minSdk = 26 and Kotlin 2.3.21 in the consuming app. ML Kit Prompt
API beta4 is built with Kotlin 2.3 metadata. See the repository's Android
example for a complete configuration. Use the current compiler DSL:
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}
The plugin supplies com.google.mlkit:genai-prompt:1.0.0-beta4 and its
transitive dependencies; do not copy older ML Kit version pins into the app.
Compatible AICore hardware and model availability are required for inference.
API 26 is an installation floor, not a hardware compatibility guarantee.
Use LocalAi.availability() / LocalAi.ensureReady() and Google's current
Prompt API setup guide.
Apple setup
The native plugin's deployment floors remain iOS 13 and macOS 12, so an app can offer a fallback on older systems. Flutter and other dependencies may set a higher app floor. Inference requires iOS/macOS 26+, eligible Apple Intelligence hardware, enabled Apple Intelligence, and ready model assets.
Build with Xcode 26 or newer for Foundation Models. Xcode 26.4 plus OS 26.4
enables exact token counts. Image input is guarded behind the OS 27 SDK /
Swift 6.4 compiler and OS 27 runtime; that branch still needs validation with
Xcode 27. Always inspect LocalAi.capabilities() before enabling images.
Windows setup
No CMake or NuGet work is needed to build: flutter build windows resolves
the Windows App SDK's C++/WinRT projection on its own. It looks for
Microsoft.WindowsAppSDK.AI and Microsoft.Windows.CppWinRT in the local
NuGet cache, downloads them from nuget.org into the build tree when they are
not there, generates the projection with cppwinrt.exe, and compiles the
Windows AI arm. When that cannot happen — no network and no cache — the build
prints a flutter_local_ai: warning and falls back to the unconfigured plugin,
which reports windowsAiFoundryUnconfigured at runtime instead of failing to
compile. Environment variables steer it without touching CMake:
| Variable | Effect |
|---|---|
FLUTTER_LOCAL_AI_WINDOWS_AI |
AUTO (default), ON (a missing projection fails the build) or OFF (skip, no download) |
FLUTTER_LOCAL_AI_NUGET_DOWNLOAD |
OFF to forbid the nuget.org download and rely on the NuGet cache |
FLUTTER_LOCAL_AI_WINRT_INCLUDE_DIR |
A projection you generated yourself (contains winrt/Microsoft.Windows.AI.Text.h) |
The full list, including pinning the SDK version, is in doc/platform-support.md.
Running is gated by Microsoft, not by the build, and none of it can be automated by a plugin:
- Hardware and OS. A Copilot+ PC (NPU), or an NVIDIA RTX 30+/AMD Radeon GPU with the vendor's latest driver and Developer Mode on; Windows 11 25H2 (build 26200.7309) or later.
- Package identity. Windows AI APIs refuse unpackaged processes. Package
the app as MSIX (the
msixpackage builds one from a Flutter app), then add thesystemAIModelscapability, theMicrosoft.WindowsAppRuntimeframework dependency for the SDK version you built against, and aMaxVersionTestedof at least10.0.26226.0to itsAppxManifest.xml—dart run msix:build, edit,dart run msix:pack. A plainflutter runtherefore reportsunavailableOther, andLocalAi.availabilityReason()names the activation failure. - Windows App Runtime. Framework-dependent MSIX pulls it in; otherwise install it from Microsoft's runtime installer. The stable channel also needs a Limited Access Feature token from Microsoft for Phi Silica; the experimental channel does not.
Microsoft's setup guide and troubleshooting page are the source of truth for these. Preparation may download large system-managed assets. Streaming currently delivers one final chunk while inference runs asynchronously; cancellation targets the active WinRT operation. The Windows arm compiles in CI but has not yet run on qualifying hardware — treat it as unverified until it has.
Usage
Availability also depends on hardware, model preparation, user settings and SDK configuration. Gate optional features on runtime capabilities.
Basic Usage
import 'package:flutter_local_ai/flutter_local_ai.dart';
// Initialize the AI engine
final aiEngine = FlutterLocalAi();
// Check if Local AI is available on this device
final isAvailable = await aiEngine.isAvailable();
if (!isAvailable) {
print('Local AI is not available on this device');
print('iOS/macOS: Requires iOS 26.0+ or macOS 26.0+');
print('Android: Requires API 26+ and Google AICore installed');
print('Windows: Requires a Copilot+ PC or supported GPU, Windows 11 25H2+, and a packaged app');
return;
}
// Initialize the model with custom instructions
// This is required and creates a LanguageModelSession
await aiEngine.initialize(
instructions: 'You are a helpful assistant. Provide concise answers.',
);
// Generate text with the simple method (returns just the text string)
final text = await aiEngine.generateTextSimple(
prompt: 'Write a short story about a robot',
maxTokens: 200,
);
print(text);
Advanced Usage with Configuration
import 'package:flutter_local_ai/flutter_local_ai.dart';
final aiEngine = FlutterLocalAi();
// Check availability
if (!await aiEngine.isAvailable()) {
print('Local AI is not available on this device');
return;
}
// Initialize with custom instructions
await aiEngine.initialize(
instructions: 'You are an expert in science and technology. Provide detailed, accurate explanations.',
);
// Generate text with detailed configuration
final response = await aiEngine.generateText(
prompt: 'Explain quantum computing in simple terms',
config: const GenerationConfig(
maxTokens: 300,
temperature: 0.7, // Controls randomness (0.0 = deterministic, 1.0 = very random)
topP: 0.9, // Nucleus sampling parameter
topK: 40, // Top-K sampling parameter
),
);
// Access detailed response information
print('Generated text: ${response.text}');
print('Token count: ${response.tokenCount}');
print('Generation time: ${response.generationTimeMs}ms');
Tool Calls (Apple platforms)
Tool calling lets the on-device model invoke Dart functions you define. Define tools in Dart, register them, and return JSON-serializable data from the handler:
- iOS 26.0+ / macOS 26.0+: native — tools are passed to Apple FoundationModels as
Toolobjects with a generation schema, so the model is constrained to produce valid calls. - Android: Native Dart tool callbacks are not bridged by this package.
registerToolsfails on this backend; gate ongetPlatformInfo().supportsToolCalling. Current Kotlin SDK capabilities and experimental reference APIs must not be assumed to work through Dart automatically.
Register an empty list to disable tool use again.
import 'package:flutter_local_ai/flutter_local_ai.dart';
final aiEngine = FlutterLocalAi();
await aiEngine.registerTools([
LocalAiTool(
name: 'searchBreadDatabase',
description: 'Searches a local database for bread recipes.',
parameters: const [
ToolParameter(
name: 'searchTerm',
type: ToolArgumentType.string,
description: 'Type of bread to search for',
),
ToolParameter(
name: 'limit',
type: ToolArgumentType.integer,
description: 'Number of recipes to return',
),
],
onCall: (arguments) async {
final term = arguments['searchTerm'] as String? ?? '';
final limit = (arguments['limit'] as num?)?.toInt() ?? 3;
// Replace with your own lookup logic.
return List.generate(
limit,
(index) => 'Recipe ${index + 1} for "$term"',
);
},
),
]);
await aiEngine.initialize(
instructions: 'You are a helpful baking assistant. Use tools when needed.',
);
final response = await aiEngine.generateText(
prompt: 'Find 2 sourdough recipes I might like.',
);
print(response.text);
Declarations that need more than a scalar
The flat parameters list covers scalars. When a parameter is a set of named
choices, a list, or a nested object, declare the whole thing as JSON Schema
with parameterSchema instead — the same subset GenerationConfig.schema
accepts, translated by the same native builder, so on Apple the model is
constrained to the declaration rather than asked to respect it:
LocalAiTool(
name: 'paintWall',
description: 'Paints a wall in one of the stocked colours.',
parameterSchema: const {
'type': 'object',
'properties': {
'colour': {
'description': 'One of the stocked colours',
'enum': ['red', 'green', 'blue', 'white', 'black', 'teal'],
},
'coats': {'type': 'integer'},
'trim': {
'type': 'object',
'properties': {
'colour': {'type': 'string'},
'gloss': {'type': 'boolean'},
},
'required': ['colour'],
},
},
'required': ['colour'],
},
onCall: (arguments) async => {'ok': true},
);
Supply parameters or parameterSchema, never both. The schema is validated
in Dart before the platform channel, so an unsupported construct fails with a
path-qualified ArgumentError naming the tool.
What onCall may do
-
Take as long as it needs. The native host suspends the turn for the whole of
onCalland imposes no timeout, so waiting on a human to approve an action is supported. A confirm-before-acting flow is written by returning aFuturethat completes when the user answers. -
Be cancelled.
session.stopGeneration()unwinds a suspended tool call instead of waiting for it; theFutureis abandoned, so a tool holding a resource releases it itself. -
Refuse. Throwing
LocalAiToolException— or any exception — hands the model a readable{"error": "..."}tool result rather than failing the turn, which is what a declined confirmation or a permission error should look like in an agent loop:onCall: (arguments) async { if (!await confirmWithUser()) { throw const LocalAiToolException('The user declined this action.'); } return {'ok': true}; }
Structured Outputs (Apple platforms)
Pass a JSON Schema through GenerationConfig to constrain generation to valid
JSON instead of free-form text. On Apple FoundationModels this uses the same
schema-constrained generation that powers tool calling, so the model is forced to
emit a value matching your schema.
- iOS 26.0+ / macOS 26.0+: native. The schema is translated into a
FoundationModels
GenerationSchemaand the JSON is returned inAiResponse.text; useAiResponse.jsonto get it decoded as aMap. - Windows: native, through
LanguageModel.GenerateStructuredJsonResponseAsync(Windows App SDK 2.0+). The schema is passed to the OS as JSON Schema text. A response the OS finishes but that strays from the schema throwsSTRUCTURED_OUTPUT_INVALID, with the model's text in the errordetails. Compiled in CI, not yet run on qualifying hardware. - Android: not available. ML Kit's structured output is generated at
compile time from annotated Kotlin classes (KSP); there is no runtime schema
API for a Dart map to be translated into. Supplying a
schema(orresponseFormat: ResponseFormat.json) throwsSTRUCTURED_OUTPUT_UNSUPPORTED. Gate ongetPlatformInfo().supportsStructuredOutputin cross-platform code.
Supported schema constructs: nested objects (with required), arrays (including
minItems / maxItems), string enums, and the scalar types (string,
integer, number, boolean). description is honored on properties. A schema
using any construct outside this subset is rejected with an ArgumentError in
Dart — before the platform channel — so you get a clear, path-qualified message
instead of an opaque native failure.
Supplying a schema implies JSON mode: you don't need to also set
responseFormat: ResponseFormat.json (though you can), and the value sent to the
backend is always self-consistent.
Streaming: schema-constrained output is not available through
generateTextStreamon any backend yet. Apple's native structured streaming API is not exposed by this package. Passing aschematogenerateTextStreamreturns a stream that errors immediately — usegenerateTextfor structured output, or stream without a schema.
final platform = await aiEngine.getPlatformInfo();
if (!platform.supportsStructuredOutput) {
// Fall back to text generation or a backend-specific parser.
return;
}
final response = await aiEngine.generateText(
prompt: 'Summarize this support ticket: "App crashes on launch after update."',
config: const GenerationConfig(
maxTokens: 300,
responseFormat: ResponseFormat.json, // default is ResponseFormat.text
schema: {
'type': 'object',
'properties': {
'title': {'type': 'string', 'description': 'Short headline'},
'priority': {
'enum': ['low', 'med', 'high'],
},
'tags': {
'type': 'array',
'items': {'type': 'string'},
},
},
'required': ['title'],
},
),
);
final data = response.json; // Map<String, dynamic>? — decoded JSON object
print(data?['title']);
// For a schema whose root is an array or scalar, use decodedJson instead —
// .json only returns object roots.
final value = response.decodedJson; // Object? — any decoded JSON value
Note:
ResponseFormat.jsonrequires a non-nullschema— Apple can only constrain output when given a schema to constrain it to.
Generative UI (genUI)
flutter_local_ai can turn a natural-language goal into a small, renderable UI
module entirely on-device. The local model decides which typed blocks best
express the goal and emits a JSON spec, which you can render with the
genui runtime or your own widgets.
The same typed-block schema and renderer work on Apple Foundation Models and Android Gemini Nano. Each module uses a short-lived session with its own instructions, preserving ongoing chats. Android uses native system instructions when AICore supports them, otherwise a prompt prefix. Generation uses a 900-token budget and compact JSON; output is parsed and validated before rendering. Model quality and platform capabilities still differ.
import 'package:flutter_local_ai/flutter_local_ai.dart';
final aiEngine = FlutterLocalAi();
final generator = LocalAiUiGenerator(aiEngine);
// Generate a module from a goal. Returns null on any failure (model
// unavailable, generation blocked, invalid output) so you can fall back to a
// deterministic UI.
final GenUiModuleSpec? module = await generator.generateModule(
'Save \$500 for a weekend trip',
principles: 'Keep it simple and low-pressure', // optional design steering
language: 'Italian', // optional: force all user-facing copy
onText: (raw) => print(raw), // optional: live decode for progress UI
);
if (module == null) {
// Inspect why and fall back.
debugPrint('genUI unavailable: ${generator.lastError}');
} else {
print(module.title); // e.g. "Weekend trip fund"
print(module.blocks); // typed blocks: amount, progress, ...
final json = module.toModuleJson(); // shape for your renderer
// final components = module.toComponentMaps(); // A2UI tree, as plain maps
}
// The detected backend is available for labelling.
print(generator.backend); // LocalAiBackend.androidMlKitGenAi / appleFoundationModels
A GenUiModuleSpec is a stack of typed blocks (amount, progress,
checklist, week, stat, list, lessons, reminder, calc, docs,
note) that the model picks to fit the goal. The output is validated before it
is returned, and on small on-device models a truncated response is repaired
where possible so a partial module still renders.
genUI + tool calls: generated UI the model can operate (Apple platforms)
The two features compose: generate a module with LocalAiUiGenerator, then
register the module's mutations as tools — the same on-device model that
designed the UI can now act on it from natural language ("add 50 to the trip
fund"), with your onCall handlers applying the state changes:
// 1. The generated module's state lives in your app (here: a progress block).
var value = 300.0;
// 2. Expose its mutations as tools for one chat turn.
await aiEngine.registerTools([
LocalAiTool(
name: 'add_to_progress',
description: 'Add an amount to the savings progress.',
parameters: const [
ToolParameter(
name: 'amount',
type: ToolArgumentType.number,
description: 'Amount to add',
),
],
onCall: (args) {
value += (args['amount'] as num).toDouble();
return {'ok': true, 'value': value}; // grounds the model's confirmation
},
),
]);
// 3. One generation = the whole turn: the model calls the tool, reads the
// result, and answers in plain text. Scope the registration to the turn —
// clear it afterwards so later generations (e.g. genUI) stay tool-free.
try {
final res = await aiEngine.generateText(
prompt: 'CURRENT STATE: {"value": $value, "target": 600}\n\n'
'USER MESSAGE: "add 50 to my trip fund"',
instructions: 'You operate a savings tracker. Use the tools to apply '
'changes, then confirm in one short sentence.',
);
print(res.text); // "Done — your trip fund is at $350 of $600."
} finally {
await aiEngine.registerTools(const []);
}
This is the pattern behind a genUI chat: hand the model the module state (the
toModuleJson() shape) plus per-block tools, and every dashboard edit the UI
can do becomes something the model can do too.
Reusing the genUI engine with other backends
The schema and parser are exposed as statics so any on-device backend (for
example a downloaded Gemma model via flutter_gemma) can drive the exact same
genUI generation:
// The system instructions (module/block schema) to pass to your own model.
final instructions = LocalAiUiGenerator.genUiInstructions;
// Parse and validate raw model text into a GenUiModuleSpec (handles code
// fences, leading/trailing prose, and truncated/closing-bracket repair).
final GenUiModuleSpec? spec = LocalAiUiGenerator.parseModelOutput(rawModelText);
Streaming Text Generation
generateTextStream yields delta chunks as the model decodes — ideal for
typing the answer into the UI live, or for the genUI generator's onText
preview. On Apple it maps to FoundationModels' streamed snapshots, on Android
to ML Kit's StreamingCallback.
final buffer = StringBuffer();
await for (final chunk in aiEngine.generateTextStream(
prompt: 'Write a two-line poem about autumn',
config: const GenerationConfig(maxTokens: 120, temperature: 0.7),
instructions: 'You are a poet.', // optional one-shot session, see below
)) {
buffer.write(chunk);
print(buffer); // cumulative text so far
}
Notes:
- The optional
instructions:parameter (also ongenerateText) runs the call in a one-shot throwaway session with exactly those instructions — nothing accumulates in the session created byinitialize. Use it for stateless callers that carry their own context in the prompt (on Apple, a shared session's transcript counts toward the 4096-token context window).
Complete Example
Here's a complete example showing error handling and best practices:
import 'package:flutter/material.dart';
import 'package:flutter_local_ai/flutter_local_ai.dart';
class LocalAiExample extends StatefulWidget {
@override
_LocalAiExampleState createState() => _LocalAiExampleState();
}
class _LocalAiExampleState extends State<LocalAiExample> {
final aiEngine = FlutterLocalAi();
bool isInitialized = false;
String? result;
bool isLoading = false;
@override
void initState() {
super.initState();
_initializeAi();
}
Future<void> _initializeAi() async {
try {
final isAvailable = await aiEngine.isAvailable();
if (!isAvailable) {
setState(() {
result = 'Local AI is not available on this device. Requires iOS 26.0+ or macOS 26.0+';
});
return;
}
await aiEngine.initialize(
instructions: 'You are a helpful assistant. Provide concise and accurate answers.',
);
setState(() {
isInitialized = true;
result = 'AI initialized successfully!';
});
} catch (e) {
setState(() {
result = 'Error initializing AI: $e';
});
}
}
Future<void> _generateText(String prompt) async {
if (!isInitialized) {
setState(() {
result = 'AI is not initialized yet';
});
return;
}
setState(() {
isLoading = true;
});
try {
final response = await aiEngine.generateText(
prompt: prompt,
config: const GenerationConfig(
maxTokens: 200,
temperature: 0.7,
),
);
setState(() {
result = response.text;
isLoading = false;
});
} catch (e) {
setState(() {
result = 'Error generating text: $e';
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter Local AI')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
ElevatedButton(
onPressed: isLoading ? null : () => _generateText('Tell me a joke'),
child: const Text('Generate Joke'),
),
const SizedBox(height: 20),
if (isLoading)
const CircularProgressIndicator()
else if (result != null)
Text(result!),
],
),
),
);
}
}
Platform-Specific Notes
iOS & macOS
- Initialization: Call
initialize()to set shared conversation instructions, or pass per-call instructions for an independent turn. - Session reuse: The session is cached and reused for subsequent generation calls until you call
initialize()again with new instructions. - Automatic fallback: If you don't call
initialize()explicitly, it will be called automatically with default instructions when you first generate text. However, it's recommended to call it explicitly to set your custom instructions. - Model availability: Requires OS 26+, eligible hardware and enabled, ready Apple Intelligence.
- Structured outputs and tool calls: Both are native FoundationModels features. Check
getPlatformInfo().supportsStructuredOutputbefore passing a schema in cross-platform code.
Android
- AICore Required: Google AICore must be installed on the device for ML Kit GenAI to work
- Availability Check: Always call
isAvailable()before using AI features - Error Handling: Handle error code -101 (AICore not installed) gracefully
- Initialization:
initialize()is optional on Android but recommended for consistency - Model Access: Uses Gemini Nano via ML Kit GenAI; model preparation can download system assets.
- Structured outputs and tool calls: ML Kit offers neither a runtime schema API nor function calling for Gemini Nano, so this package cannot bridge them; see known limitations. Gate these on capabilities.
Windows
- The build configures itself (see Windows setup); the host app owns packaging, the
systemAIModelscapability and the Windows App Runtime. - Probe availability before generation; OS version alone is insufficient, and
LocalAi.availabilityReason()names an activation failure. - Full responses and the one-chunk stream run asynchronously and can be cancelled.
- Structured output is native (
GenerateStructuredJsonResponseAsync); native tools are not exposed by Windows AI. - The arm compiles in CI; device validation on Copilot+ hardware remains a release requirement.
Example with AICore Error Handling:
final aiEngine = FlutterLocalAi();
try {
final isAvailable = await aiEngine.isAvailable();
if (!isAvailable) {
// Show user-friendly message
print('Local AI is not available. AICore may not be installed.');
return;
}
await aiEngine.initialize(
instructions: 'You are a helpful assistant.',
);
final response = await aiEngine.generateText(
prompt: 'Hello!',
config: const GenerationConfig(maxTokens: 100),
);
print(response.text);
} catch (e) {
// Handle AICore error (-101)
if (e.toString().contains('-101') || e.toString().contains('AICore')) {
// Open Play Store to install AICore
await aiEngine.openAICorePlayStore();
} else {
print('Error: $e');
}
}
Known limitations and fallbacks
Each of these is a property of the vendor API, not a gap this package can
close on its own. In every case the right move is to ask
LocalAi.availability() / LocalAi.capabilities() at runtime and fall back
to another backend — the flutter_gemma bridge above being the obvious one.
- iOS and macOS below 26. Apple Foundation Models exist only from OS 26,
and no polyfill can conjure the system model on iOS 17 or 18. The package
still installs and links on older OSes (its deployment floor is iOS 13 /
macOS 12):
LocalAi.availability()returnsunavailableOsTooOld,capabilities()reports every feature as unsupported, and a call that needs the model throws aLocalAiUnavailableExceptionrather than crashing. Ship a bundled-model fallback for those users, or gate the feature. - Android tool calls and structured output. ML Kit's Prompt API has no function-calling surface for Gemini Nano, and its structured output is compiled from annotated Kotlin classes with KSP — there is no runtime schema object for a Dart map to become. A prompt-woven emulation was tried and rejected: Gemini Nano answered in prose instead of performing the call often enough that the capability flag would have lied.
- Windows tool calls. Windows AI Foundry exposes no function-calling API.
- Bring-your-own models on Android and Windows. Out of scope by design:
this package is the OS-model layer, deliberately without a model
downloader, GGUF loader or inference runtime. For Llama, Phi, Qwen and
friends use flutter_gemma, whose
flutter_gemma_builtin_aibridge lets the OS model and a downloaded model sit behind one interface.
API Reference
FlutterLocalAi
Main class for interacting with local AI.
Methods
Future<bool> isAvailable()- Check if local AI is available on the deviceFuture<String> availabilityReason()- A human-readable reason when it is not (eligibility, model downloadable/downloading, OS version…)Future<bool> initialize({String? instructions})- Initialize the model and create a session with instruction text (required for iOS, recommended for Android)Future<AiResponse> generateText({required String prompt, GenerationConfig? config, String? instructions})- Generate text; per-callinstructionsrun a one-shot throwaway sessionStream<String> generateTextStream({required String prompt, GenerationConfig? config, String? instructions})- Generate text as a stream of delta chunksFuture<String> generateTextSimple({required String prompt, int maxTokens = 100})- Convenience method to generate text and return just the stringFuture<void> registerTools(List<LocalAiTool> tools)- Register Dart tools the model may call during generation (Apple platforms only; passconst []to clear)Future<LocalAiPlatformInfo> getPlatformInfo()- The detected backend and its capabilities (supportsToolCalling,supportsStructuredOutput,supportsModelDownload, …)Future<ModelFeatureStatus> getModelStatus()-available/downloadable/downloading/unavailable(Android Gemini Nano)Stream<ModelDownloadStatus> downloadModel()- Request the one-time on-device model download and observe its progress; failures always surface on the streamFuture<bool> openAICorePlayStore()- Open Google AICore in the Play Store (Android only, useful when error -101 occurs)
LocalAiTool / ToolParameter
A Dart-defined tool the on-device model can invoke (see Tool Calls above).
LocalAiTool({required name, required description, parameters, parameterSchema, required onCall})-onCallreceives the model's arguments as aMap<String, dynamic>and returns JSON-serializable data fed back to the model. Declare parameters as either a flatparameterslist or aparameterSchema, not bothToolParameter({required name, type, description, optional})- typed scalar parameter (ToolArgumentType.string/integer/number/boolean)parameterSchema(Map<String, dynamic>?) - the parameters as a JSON Schema object, for nested objects, arrays and string enums. Validated in Dart, then translated natively by the same builder that backs structured outputresolvedParameterSchema- the declaration actually sent:parameterSchema, or the object schema the flat list describesLocalAiToolException(message, {details})- thrown fromonCallto hand the model a readable tool error instead of failing the turn. Any other exception is reported the same way
GenerationConfig
Configuration for text generation.
maxTokens(int, default: 100) - Maximum number of tokens to generatetemperature(double?, optional) - Temperature for generation (0.0 to 1.0)topP(double?, optional) - Top-p (nucleus) sampling. On Apple maps to.random(probabilityThreshold:)topK(int?, optional) - Top-k sampling. On Apple maps to.random(top:)and takes precedence overtopPresponseFormat(ResponseFormat, default:text) -textfor free-form output, orjsonfor schema-constrained JSON (Apple only).jsonrequires a non-nullschemaschema(Map<String, dynamic>?, optional) - JSON Schema the output is constrained to (Apple only; see Structured Outputs above). Supplying a schema implies JSON mode and is validated in Dart before the platform channel
Sampling precedence on Apple: topK > topP > temperature > greedy.
.greedyis never combined with a temperature (that pairing throws on-device).
AiResponse
Response from AI generation.
text(String) - The generated text (or the JSON string when structured output was requested)json(Map<String, dynamic>?) -textdecoded as a JSON object, ornullif it isn't onedecodedJson(Object?) -textdecoded as any JSON value (object, array, scalar), ornullif it isn't valid JSON — use for schemas whose root is an array or scalartokenCount(int?) - Token count usedgenerationTimeMs(int?) - Generation time in milliseconds
LocalAiPlatformInfo
Detected backend metadata and capability flags returned by getPlatformInfo().
backend(LocalAiBackend) - The active backend (appleFoundationModels,androidMlKitGenAi,windowsAiFoundry,windowsAiFoundryUnconfigured, orunsupported)supportsToolCalling(bool) - WhetherregisterToolscan expose Dart tools to the native modelsupportsStructuredOutput(bool) - WhetherGenerationConfig.schema/ResponseFormat.jsoncan constrain the model to JSON outputsupportsModelDownload(bool) - WhetherdownloadModel()can request an on-device model downloadsupportsPlayStoreRedirect(bool) - WhetheropenAICorePlayStore()can redirect to Google AICoreisConfigured(bool) - Whether the native backend is compiled/configured for the current platform
LocalAiUiGenerator
Turns a natural-language goal into a GenUiModuleSpec using the on-device model
(Apple FoundationModels or Android ML Kit GenAI / Gemini Nano).
LocalAiUiGenerator([FlutterLocalAi? ai])- Create a generator (reuses or creates an engine)Future<GenUiModuleSpec?> generateModule(String goal, {String? principles, String? language, void Function(String)? onText})- Generate a module;languageforces all user-facing copy into that language,onTextstreams the raw decode for live progress UI; returnsnullon any failureLocalAiBackend get backend- The detected on-device backendbool? get available- Whether the model reported itself available (cached)String? get lastError- The last platform error encountered, for diagnosticsstatic String get genUiInstructions- The module/block schema instructions, for reuse with other backendsstatic GenUiModuleSpec? parseModelOutput(String text)- Parse + validate raw model text (handles fences, prose, truncation)
GenUiModuleSpec
A validated genUI module produced by the local model.
title,icon,tone,blurb(String) - Module header fieldsblocks(List<Map<String, dynamic>>) - Ordered typed blocksMap<String, dynamic> toModuleJson()- Shape for a typed-block rendererList<Map<String, dynamic>> toComponentMaps()- An A2UI component tree as plainid/type/propertiesmaps. Wrap each entry ingenui'sComponentto feed aSurface— see the doc comment for the three-line adaptation.
Implementation notes
Both public Dart APIs — and any external adapter written against them — use one session host per platform. Each conversation has a distinct ID, and the native resource remains alive until all model owners close. Android serializes generation over the AICore client; Apple keeps separate Foundation Models sessions. Windows uses C++/WinRT asynchronous operations from the Flutter runner's STA.
Apple translates the supported dynamic JSON Schema subset to
GenerationSchema and binds Dart tools at session creation; Windows and web
hand the JSON Schema text to the OS (GenerateStructuredJsonResponseAsync
and responseConstraint). Android's Kotlin/KSP structured output has no
runtime form to bridge. See the
platform coverage and remaining gaps
for current API versions, supported features, and what is still unverified.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Libraries
- flutter_local_ai
- flutter_local_ai_web
- testing
- Test doubles for code built on flutter_local_ai.