Flutter Local ML Model Inference: A Practical, Production-Ready Guide
A practical guide to running fast, private, offline ML inference in Flutter using TFLite, ONNX Runtime, and PyTorch Mobile.
Image used for representation purposes only.
Overview
On-device machine learning (ML) in Flutter unlocks fast, private, and offline experiences—from real‑time vision to audio and NLP. This guide shows how to run local inference in Flutter using mainstream runtimes (TensorFlow Lite, ONNX Runtime, and PyTorch Mobile), how to package and update models, and how to optimize performance and reliability for production apps.
Why local inference?
- Low latency: Avoid network round trips; keep interactions within the 16 ms/frame UI budget where possible.
- Privacy: Keep user data on device; minimize regulatory risk and user mistrust.
- Resilience: Work offline and degrade gracefully during network loss.
- Cost control: Reduce server inference costs and egress fees.
Architectural choices
Flutter’s UI runs in the Dart VM, while native ML runtimes exist as Android/iOS libraries. You integrate them via:
- Platform plugins: Prebuilt bindings that expose a Dart API to native runtimes.
- FFI (Foreign Function Interface): Call C/C++ APIs directly from Dart for fine-grained control.
- Platform channels: Send messages to small native wrappers that call ML libraries.
Most teams start with a plugin, then adopt FFI or custom channels when they need advanced delegates, custom ops, or tight memory control.
Common runtimes for Flutter
- TensorFlow Lite (TFLite): Lightweight, mobile-first; broad hardware delegate support (NNAPI on Android, Metal/GPU on iOS, XNNPACK CPU).
- ONNX Runtime Mobile: Good for models exported to ONNX; selective operator builds keep size down.
- PyTorch (Mobile/ Lite Interpreter): Useful if your training stack is PyTorch and you want TorchScript models.
- ML Kit (on-device APIs): Turnkey vision/NLP features without managing your own model files; less flexible for custom architectures.
Pick based on your training framework, model format, target hardware, and the need for custom ops.
Packaging the model
-
As an asset (simplest):
- Place
assets/models/model.tflite(or.onnx/.ptl) in your project. - Reference it in
pubspec.yaml:flutter: assets: - assets/models/model.tflite - Pros: Easy distribution; no first-run download.
- Cons: App update required to change the model; increases APK/IPA size.
- Place
-
Dynamic download:
- Host the model on a CDN, verify its hash, download on first run, and cache under
getApplicationSupportDirectory(). - Pros: Update models without shipping a new app.
- Cons: Add remote config, integrity checks, and migration logic.
- Host the model on a CDN, verify its hash, download on first run, and cache under
Tip: Keep models under 50–100 MB for mobile; split per-ABI on Android to avoid shipping all native binaries to every device.
Threading and isolates
Inference must not block Flutter’s main isolate (UI thread). Use:
Isolate.run(Dart 3+) for synchronous compute off the main isolate.computefor simple background tasks.- Streams/EventChannels when the native side pushes frames/results (e.g., camera pipelines).
Avoid sharing a single interpreter across isolates/threads concurrently unless the runtime guarantees thread safety. Create one interpreter per worker or use explicit serialization.
Example: TensorFlow Lite with tflite_flutter
Below is a minimal classifier that runs on a background isolate, supports hardware delegates, and loads a model from assets.
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/services.dart' show rootBundle;
import 'package:tflite_flutter/tflite_flutter.dart';
class TfliteClassifier {
final String assetPath;
Interpreter? _interpreter;
TfliteClassifier(this.assetPath);
Future<void> init() async {
final options = InterpreterOptions()
..threads = (Platform.numberOfProcessors.clamp(2, 8))
..useNnApiForAndroid = Platform.isAndroid; // CPU accel; safe default
// Try a GPU/Metal delegate where available; swallow if not supported
try {
if (Platform.isAndroid) {
options.addDelegate(GpuDelegateV2());
} else if (Platform.isIOS) {
options.addDelegate(GpuDelegate()); // uses Metal under the hood
}
} catch (_) {
// Fallback to CPU
}
_interpreter = await Interpreter.fromAsset(assetPath, options: options);
}
Future<List<double>> run(List<double> input, List<int> inputShape) async {
if (_interpreter == null) throw StateError('Interpreter not initialized');
// Allocate tensors only once in production; shown inline for clarity
_interpreter!.allocateTensors();
// Reshape input if dynamic; ensure types match the model (e.g., float32 vs int8)
var inputTensor = TensorBuffer.createFixedSize(inputShape, TfLiteType.float32);
inputTensor.loadList(input);
// Prepare output buffer; adapt to your model’s output shape
final output = List.filled(1001, 0.0).reshape([1, 1001]);
await Isolate.run(() {
_interpreter!.run(inputTensor.getFloatList(), output);
});
return List<double>.from(output.first);
}
void close() => _interpreter?.close();
}
Notes:
- Replace
GpuDelegateV2/GpuDelegatewith your target-specific delegates as needed. - Prefer allocating tensors once, reusing input/output buffers per frame.
- If your model is quantized (int8/uint8), convert inputs accordingly and dequantize outputs using the tensor’s scale/zero-point.
Example: Loading a downloaded model file
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:tflite_flutter/tflite_flutter.dart';
Future<Interpreter> loadDownloadedModel(String fileName) async {
final dir = await getApplicationSupportDirectory();
final path = '${dir.path}/$fileName';
final options = InterpreterOptions()..threads = 4;
return Interpreter.fromFile(File(path), options: options);
}
Add integrity checks (e.g., SHA‑256) before trusting the model. Store and compare expected hashes obtained from your backend or remote config.
Example: ONNX Runtime Mobile via FFI
ONNX Runtime exposes a C API, which Flutter can call using FFI through community plugins. The high-level flow:
- Initialize the environment and session with mobile build settings.
- Load the
.onnxmodel from assets or disk. - Create input tensors and run
Session.run().
This path is powerful if your training/export pipeline already outputs ONNX or you rely on operators that map more naturally to ONNX Runtime.
Example: PyTorch Mobile
For TorchScript (.pt/.ptl) models, a Flutter plugin can host a minimal native wrapper that:
- Loads the TorchScript module.
- Converts inputs (e.g.,
Float32Listimage tensors) to native tensor types. - Invokes
forward()and returns outputs through a channel or FFI.
Torch-based pipelines can be convenient if you need parity with existing PyTorch code and quantization workflows like QAT.
Camera + real‑time inference pipeline
For vision tasks:
- Use
cameraplugin to stream YUV/RGB frames. - Convert frames to the model’s input size and color space in native code or with SIMD-optimized libraries; avoid per-frame heap churn in Dart.
- Batch work on a fixed-rate scheduler (e.g., every 2–3 frames) to maintain 60 FPS UI.
- Pin CPU/GPU delegates early and reuse interpreters.
Performance fundamentals
- Model choice: Prefer mobile‑friendly backbones (MobileNetV3, EfficientNet‑Lite, MobileViT, etc.).
- Quantization:
- Post‑training dynamic range (weight‑only): Smallest effort, largest gains for CPU.
- Full‑integer (int8 activations/weights): Great for NNAPI/TPU‑like accelerators.
- Float16: Useful on GPUs; lower precision with minimal accuracy drop.
- Delegates:
- Android: NNAPI for hardware acceleration, GPU delegate for shader‑friendly ops, XNNPACK for fast CPU.
- iOS: Metal (via GPU delegate) for many conv/activation patterns; Accelerate for CPU.
- Threads: Tune
InterpreterOptions.threadsempirically; more threads ≠ always faster due to contention. - Warmup: Run a few dummy inferences during startup to JIT/warm caches and allocate buffers.
- Memory: Reuse input/output buffers; avoid copying large tensors across isolates.
- Release builds: Benchmark on profile/release; debug mode skews timing.
Benchmarking
- Use
Stopwatcharound only therun()call; measure preprocessing and postprocessing separately. - Collect p50/p90/p99 latencies across device classes (low, mid, flagship) and both cold and warm states.
- Monitor energy impact with Android Studio Profiler and Xcode Energy Log; throttling can mask perf gains.
Model updates and rollouts
- Version your models (e.g.,
model_v7_int8.tflite) and expose a JSON manifest with:- Version, SHA‑256, size, min app version, supported ABIs.
- Optional rollout percentage for staged releases.
- On startup, check for a newer version, validate integrity, then atomically swap files.
- Keep the previous model for fast rollback.
Security and privacy
- Integrity: Verify checksums and, if feasible, sign model files.
- Obfuscation: You can obfuscate model names and apply light encryption; determined actors can still reverse‑engineer.
- PII: Keep inputs/outputs local; avoid logging raw frames or embeddings.
- Policy: Document on-device inference in your privacy policy; offer opt‑outs for analytics.
Handling unsupported operators
- TFLite: Convert with
SELECT_TF_OPSonly if unavoidable; it increases binary size. Prefer re‑architecting to supported ops. - ONNX: Build a mobile package with just the needed kernels to keep the footprint small.
- PyTorch: Ensure your TorchScript avoids custom Python-only ops.
Build and size tips
- Android:
- Enable ABI splits to avoid shipping all native libraries:
android { splits { abi { enable true reset() include 'armeabi-v7a', 'arm64-v8a', 'x86_64' universalApk false } } } - Use R8/ProGuard; keep native symbols off release unless you need crash symbolication.
- Enable ABI splits to avoid shipping all native libraries:
- iOS:
- Use release builds with bitcode disabled (modern Xcode toolchains); ensure Metal is available for GPU delegate devices.
- Exclude simulator slices from your app store build to save size.
Testing matrix
- CPU/GPU/NNAPI/Metal variations across:
- Android 8–14, armv7/arm64, with/without Play Services.
- iOS 14–latest, A‑series devices across tiers.
- Stress with background tasks (music, downloads) to simulate real user conditions.
Troubleshooting checklist
- Inference is slow: Reduce input size, quantize, enable XNNPACK/NNAPI/Metal, batch every N frames.
- Crashes on delegate init: Catch/disable the delegate and fall back to CPU; log device/GPU details for telemetry.
- Dimension mismatch: Inspect interpreter input/output shapes at runtime; resize inputs accordingly.
- Out-of-memory: Reuse buffers, downscale images, and avoid holding multiple interpreters per screen.
When to pick each runtime
- TFLite: Best default for mobile; strong tooling, delegates, and small model formats.
- ONNX Runtime Mobile: If your pipeline is ONNX-centric or you need operators that map cleanly to ONNX.
- PyTorch Mobile: If you train in PyTorch and need TorchScript parity, especially with QAT workflows.
- ML Kit: If you want production-grade vision/NLP features quickly and can accept less flexibility.
Production-ready template (high level)
- App startup:
- Load model manifest → validate local cache → warmup interpreter.
- Inference service:
- Singleton per model with queueing; background isolate; adaptive rate control.
- Telemetry:
- Log inference latency (p50/p90), delegate in use, and fallbacks; no raw user data.
- Rollout:
- Staged percentage with rollback on regression; keep N-1 model for safety.
Conclusion
Local ML inference in Flutter is mature and production-ready. Start with TFLite for most workloads, consider ONNX Runtime or PyTorch Mobile based on your training stack, and design for isolates, delegates, and staged updates from the outset. With smart quantization and careful pipeline engineering, you can deliver private, real-time ML experiences that feel native and scale to millions of users—without a single server in the loop.
Related Posts
Flutter + TensorFlow Lite: Local AI Integration Guide
A practical guide to integrating TensorFlow Lite models into Flutter for fast, private, offline on-device AI with performance tuning and code examples.
Flutter Dynamic Feature Delivery: A Practical, End‑to‑End Guide
A practical guide to Flutter dynamic feature delivery: design, deferred imports, Android deferred components, UX patterns, testing, and pitfalls.
Edge AI On-Device Inference Tutorial: From Model to Real-Time App
Build and deploy an edge AI model on-device: train, quantize to TFLite, and run on Raspberry Pi and Android with real-time profiling and optimization.