Start a project

TensorFlow Lite in Flutter: Building Smart Mobile Apps

Complete guide to implementing on-device machine learning models in Flutter apps using TensorFlow Lite. Learn image classification, object detection, and custom model deployment with real-world examples.

Introduction to On-Device ML

In the era of mobile-first applications, on-device machine learning has become crucial for creating responsive, privacy-conscious, and intelligent apps. TensorFlow Lite brings the power of machine learning directly to mobile devices, enabling real-time inference without requiring an internet connection. This comprehensive guide will walk you through implementing TensorFlow Lite models in your Flutter applications.

TensorFlow Lite Mobile ML

On-device ML offers several advantages over cloud-based solutions: lower latency, offline functionality, enhanced privacy, and reduced server costs. With Flutter's cross-platform capabilities and TensorFlow Lite's optimized runtime, you can deploy sophisticated ML models across iOS and Android with a single codebase.

Why TensorFlow Lite for Flutter?

TensorFlow Lite is specifically designed for mobile and embedded devices, offering several key advantages for Flutter developers:

Model Type Size (MB) Inference Time (ms) Accuracy Use Case
MobileNetV2 14 30-50 71.8% Image Classification
SSD MobileNet 27 50-80 mAP: 22 Object Detection
PoseNet 13 40-60 - Pose Estimation
DeepLab v3 2.7 100-150 mIOU: 77.21 Segmentation

Setting Up TensorFlow Lite

Let's start by setting up TensorFlow Lite in your Flutter project. We'll use the official TensorFlow Lite Flutter plugin and helper package.

1. Add Dependencies

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  tflite_flutter: ^0.10.0
  tflite_flutter_helper: ^0.4.0
  image_picker: ^1.0.0
  camera: ^0.10.0
  path_provider: ^2.0.0
  image: ^4.0.0

2. Platform-Specific Setup

💡 Tip: For iOS, you may need to add the TensorFlow Lite framework manually. Follow the official documentation for detailed iOS setup instructions.

Android Configuration

// android/app/build.gradle
android {
    ...
    aaptOptions {
        noCompress 'tflite'
        noCompress 'lite'
    }
}

iOS Configuration

# ios/Podfile
platform :ios, '11.0'

# Add this at the end
pod 'TensorFlowLiteSwift'

3. Model Preparation

Place your .tflite model files in the assets folder:

# pubspec.yaml
flutter:
  assets:
    - assets/models/mobilenet_v2.tflite
    - assets/models/labels.txt

Image Classification Implementation

Let's implement a complete image classification solution using MobileNet V2. This example demonstrates how to load a model, preprocess images, and interpret results.

Creating the Classifier Service

import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:tflite_flutter/tflite_flutter.dart';
import 'package:tflite_flutter_helper/tflite_flutter_helper.dart';
import 'package:image/image.dart' as img;

class ImageClassifier {
  late Interpreter _interpreter;
  late List _labels;
  late ImageProcessor _imageProcessor;
  
  static const String modelFile = 'assets/models/mobilenet_v2.tflite';
  static const String labelsFile = 'assets/models/labels.txt';
  
  // Model input/output dimensions
  static const int inputSize = 224;
  static const int numResults = 5;
  static const double threshold = 0.1;
  
  ImageClassifier() {
    _loadModel();
    _loadLabels();
    _setupImageProcessor();
  }
  
  Future _loadModel() async {
    try {
      // Load model from assets
      _interpreter = await Interpreter.fromAsset(modelFile);
      print('Model loaded successfully');
    } catch (e) {
      print('Failed to load model: $e');
      throw Exception('Failed to load TensorFlow Lite model');
    }
  }
  
  Future _loadLabels() async {
    try {
      final labelsData = await rootBundle.loadString(labelsFile);
      _labels = labelsData.split('\n').where((label) => label.isNotEmpty).toList();
      print('Loaded ${_labels.length} labels');
    } catch (e) {
      print('Failed to load labels: $e');
      throw Exception('Failed to load labels');
    }
  }
  
  void _setupImageProcessor() {
    // Define preprocessing steps
    _imageProcessor = ImageProcessorBuilder()
        .add(ResizeOp(inputSize, inputSize, ResizeMethod.BILINEAR))
        .add(NormalizeOp(0, 255)) // Normalize to [0, 1]
        .add(QuantizeOp(0, 1))    // Quantize if using quantized model
        .build();
  }
  
  Future> classifyImage(File imageFile) async {
    try {
      // Load and decode image
      final imageBytes = await imageFile.readAsBytes();
      img.Image? image = img.decodeImage(imageBytes);
      
      if (image == null) {
        throw Exception('Failed to decode image');
      }
      
      // Preprocess the image
      final inputImage = TensorImage.fromImage(image);
      final processedImage = _imageProcessor.process(inputImage);
      
      // Prepare output tensor
      final outputShape = _interpreter.getOutputTensor(0).shape;
      final outputType = _interpreter.getOutputTensor(0).type;
      final outputBuffer = TensorBuffer.createDynamic(outputType);
      
      // Run inference
      _interpreter.run(processedImage.buffer, outputBuffer.buffer);
      
      // Process results
      return _processResults(outputBuffer);
    } catch (e) {
      print('Classification failed: $e');
      throw Exception('Failed to classify image');
    }
  }
  
  List _processResults(TensorBuffer outputBuffer) {
    // Get probabilities from output
    final probabilities = outputBuffer.getDoubleList();
    
    // Create list of recognitions
    final recognitions = [];
    for (int i = 0; i < probabilities.length; i++) {
      if (probabilities[i] >= threshold) {
        recognitions.add(Recognition(
          id: i,
          label: _labels[i],
          confidence: probabilities[i],
        ));
      }
    }
    
    // Sort by confidence
    recognitions.sort((a, b) => b.confidence.compareTo(a.confidence));
    
    // Return top results
    return recognitions.take(numResults).toList();
  }
  
  void dispose() {
    _interpreter.close();
  }
}

class Recognition {
  final int id;
  final String label;
  final double confidence;
  
  Recognition({
    required this.id,
    required this.label,
    required this.confidence,
  });
  
  String get displayText => '${(confidence * 100).toStringAsFixed(1)}% $label';
}

Building the UI

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';

class ImageClassificationScreen extends StatefulWidget {
  @override
  _ImageClassificationScreenState createState() => _ImageClassificationScreenState();
}

class _ImageClassificationScreenState extends State {
  final ImageClassifier _classifier = ImageClassifier();
  final ImagePicker _picker = ImagePicker();
  
  File? _image;
  List? _results;
  bool _isProcessing = false;
  
  Future _pickImage(ImageSource source) async {
    try {
      final XFile? pickedFile = await _picker.pickImage(source: source);
      
      if (pickedFile != null) {
        setState(() {
          _image = File(pickedFile.path);
          _isProcessing = true;
        });
        
        // Classify the image
        final results = await _classifier.classifyImage(_image!);
        
        setState(() {
          _results = results;
          _isProcessing = false;
        });
      }
    } catch (e) {
      setState(() {
        _isProcessing = false;
      });
      _showError('Failed to process image: $e');
    }
  }
  
  void _showError(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message), backgroundColor: Colors.red),
    );
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Image Classification'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Column(
        children: [
          // Image display area
          Expanded(
            child: Container(
              margin: EdgeInsets.all(20),
              decoration: BoxDecoration(
                border: Border.all(color: Colors.grey),
                borderRadius: BorderRadius.circular(10),
              ),
              child: _image == null
                  ? Center(
                      child: Text(
                        'No image selected',
                        style: TextStyle(color: Colors.grey),
                      ),
                    )
                  : Stack(
                      fit: StackFit.expand,
                      children: [
                        Image.file(_image!, fit: BoxFit.contain),
                        if (_isProcessing)
                          Container(
                            color: Colors.black54,
                            child: Center(
                              child: CircularProgressIndicator(),
                            ),
                          ),
                      ],
                    ),
            ),
          ),
          
          // Results display
          if (_results != null && _results!.isNotEmpty)
            Container(
              height: 200,
              padding: EdgeInsets.all(20),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Predictions:',
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 10),
                  Expanded(
                    child: ListView.builder(
                      itemCount: _results!.length,
                      itemBuilder: (context, index) {
                        final result = _results![index];
                        return ListTile(
                          title: Text(result.label),
                          trailing: Text(
                            '${(result.confidence * 100).toStringAsFixed(1)}%',
                            style: TextStyle(fontWeight: FontWeight.bold),
                          ),
                          leading: Container(
                            width: 50,
                            child: LinearProgressIndicator(
                              value: result.confidence,
                              backgroundColor: Colors.grey[300],
                              valueColor: AlwaysStoppedAnimation(
                                result.confidence > 0.5 ? Colors.green : Colors.orange,
                              ),
                            ),
                          ),
                        );
                      },
                    ),
                  ),
                ],
              ),
            ),
          
          // Action buttons
          Padding(
            padding: EdgeInsets.all(20),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton.icon(
                  onPressed: () => _pickImage(ImageSource.camera),
                  icon: Icon(Icons.camera_alt),
                  label: Text('Camera'),
                ),
                ElevatedButton.icon(
                  onPressed: () => _pickImage(ImageSource.gallery),
                  icon: Icon(Icons.photo_library),
                  label: Text('Gallery'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
  
  @override
  void dispose() {
    _classifier.dispose();
    super.dispose();
  }
}

Real-Time Object Detection

Now let's implement real-time object detection using the camera feed. We'll use SSD MobileNet for detecting multiple objects in real-time.

Object Detection Service

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:tflite_flutter/tflite_flutter.dart';

class ObjectDetector {
  late Interpreter _interpreter;
  late List _labels;
  
  static const String modelFile = 'assets/models/ssd_mobilenet.tflite';
  static const String labelsFile = 'assets/models/coco_labels.txt';
  
  // Model parameters
  static const int inputSize = 300;
  static const int numDetections = 10;
  static const double minConfidence = 0.5;
  
  Future initialize() async {
    // Load model
    _interpreter = await Interpreter.fromAsset(modelFile);
    
    // Load labels
    final labelsData = await rootBundle.loadString(labelsFile);
    _labels = labelsData.split('\n');
  }
  
  Future> detectObjects(CameraImage image) async {
    // Convert CameraImage to input tensor
    final input = _preprocessImage(image);
    
    // Prepare output tensors
    final outputLocations = List.filled(1 * numDetections * 4, 0.0).reshape([1, numDetections, 4]);
    final outputClasses = List.filled(1 * numDetections, 0.0).reshape([1, numDetections]);
    final outputScores = List.filled(1 * numDetections, 0.0).reshape([1, numDetections]);
    final numDetections = List.filled(1, 0.0).reshape([1]);
    
    // Create output map
    final outputs = {
      0: outputLocations,
      1: outputClasses,
      2: outputScores,
      3: numDetections,
    };
    
    // Run inference
    _interpreter.runForMultipleInputs([input], outputs);
    
    // Process detections
    return _processDetections(
      outputLocations[0],
      outputClasses[0],
      outputScores[0],
    );
  }
  
  List>> _preprocessImage(CameraImage image) {
    // Convert YUV420 to RGB and resize
    final convertedImage = _convertYUV420ToImage(image);
    final resizedImage = _resizeImage(convertedImage, inputSize);
    
    // Normalize pixel values
    return _normalizeImage(resizedImage);
  }
  
  List _processDetections(
    List> locations,
    List classes,
    List scores,
  ) {
    final detections = [];
    
    for (int i = 0; i < numDetections; i++) {
      if (scores[i] >= minConfidence) {
        final rect = Rect.fromLTRB(
          locations[i][1],
          locations[i][0],
          locations[i][3],
          locations[i][2],
        );
        
        detections.add(Detection(
          label: _labels[classes[i].toInt()],
          confidence: scores[i],
          boundingBox: rect,
        ));
      }
    }
    
    return detections;
  }
  
  void dispose() {
    _interpreter.close();
  }
}

class Detection {
  final String label;
  final double confidence;
  final Rect boundingBox;
  
  Detection({
    required this.label,
    required this.confidence,
    required this.boundingBox,
  });
}

Real-Time Camera View

class ObjectDetectionScreen extends StatefulWidget {
  @override
  _ObjectDetectionScreenState createState() => _ObjectDetectionScreenState();
}

class _ObjectDetectionScreenState extends State {
  CameraController? _cameraController;
  ObjectDetector? _detector;
  List _detections = [];
  bool _isDetecting = false;
  
  @override
  void initState() {
    super.initState();
    _initializeCamera();
    _initializeDetector();
  }
  
  Future _initializeCamera() async {
    final cameras = await availableCameras();
    final camera = cameras.first;
    
    _cameraController = CameraController(
      camera,
      ResolutionPreset.medium,
      enableAudio: false,
    );
    
    await _cameraController!.initialize();
    
    _cameraController!.startImageStream((CameraImage image) {
      if (!_isDetecting) {
        _isDetecting = true;
        _detectObjects(image);
      }
    });
    
    setState(() {});
  }
  
  Future _initializeDetector() async {
    _detector = ObjectDetector();
    await _detector!.initialize();
  }
  
  Future _detectObjects(CameraImage image) async {
    try {
      final detections = await _detector!.detectObjects(image);
      
      setState(() {
        _detections = detections;
      });
    } finally {
      _isDetecting = false;
    }
  }
  
  @override
  Widget build(BuildContext context) {
    if (_cameraController == null || !_cameraController!.value.isInitialized) {
      return Scaffold(
        body: Center(child: CircularProgressIndicator()),
      );
    }
    
    return Scaffold(
      body: Stack(
        fit: StackFit.expand,
        children: [
          CameraPreview(_cameraController!),
          CustomPaint(
            painter: DetectionPainter(_detections),
          ),
          Positioned(
            bottom: 20,
            left: 20,
            right: 20,
            child: Container(
              padding: EdgeInsets.all(10),
              decoration: BoxDecoration(
                color: Colors.black54,
                borderRadius: BorderRadius.circular(10),
              ),
              child: Text(
                'Detected: ${_detections.length} objects',
                style: TextStyle(color: Colors.white, fontSize: 16),
                textAlign: TextAlign.center,
              ),
            ),
          ),
        ],
      ),
    );
  }
  
  @override
  void dispose() {
    _cameraController?.dispose();
    _detector?.dispose();
    super.dispose();
  }
}

Training Custom Models

While pre-trained models are great for general use cases, you'll often need custom models for specific applications. Here's how to train and deploy custom TensorFlow Lite models.

Training with TensorFlow

# train_custom_model.py
import tensorflow as tf
from tensorflow import keras
import numpy as np

# Load and prepare dataset
(x_train, y_train), (x_test, y_test) = keras.datasets.cifar10.load_data()
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0

# Define model architecture
model = keras.Sequential([
    keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(32, 32, 3)),
    keras.layers.MaxPooling2D((2, 2)),
    keras.layers.Conv2D(64, (3, 3), activation='relu'),
    keras.layers.MaxPooling2D((2, 2)),
    keras.layers.Conv2D(64, (3, 3), activation='relu'),
    keras.layers.Flatten(),
    keras.layers.Dense(64, activation='relu'),
    keras.layers.Dense(10, activation='softmax')
])

# Compile and train
model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

model.fit(
    x_train, y_train,
    epochs=10,
    validation_data=(x_test, y_test)
)

# Convert to TensorFlow Lite
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_model = converter.convert()

# Save the model
with open('custom_model.tflite', 'wb') as f:
    f.write(tflite_model)
    
print("Model converted and saved successfully!")

Model Quantization

# Quantization for smaller model size and faster inference
def representative_dataset():
    for _ in range(100):
        data = np.random.rand(1, 32, 32, 3).astype(np.float32)
        yield [data]

converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [
    tf.lite.OpsSet.TFLITE_BUILTINS_INT8
]
converter.inference_input_type = tf.uint8
converter.inference_output_type = tf.uint8

tflite_quant_model = converter.convert()

Model Optimization Techniques

Optimizing your models for mobile deployment is crucial for performance. Here are key techniques to reduce model size and improve inference speed.

1. Quantization

💡 Tip: Quantization can reduce model size by up to 4x with minimal accuracy loss (typically < 2%).
// Using quantized models in Flutter
class QuantizedModelHelper {
  static Float32List quantizeInput(Float32List input, double scale, int zeroPoint) {
    final quantized = Float32List(input.length);
    for (int i = 0; i < input.length; i++) {
      quantized[i] = (input[i] / scale + zeroPoint).clamp(0, 255);
    }
    return quantized;
  }
  
  static Float32List dequantizeOutput(Uint8List output, double scale, int zeroPoint) {
    final dequantized = Float32List(output.length);
    for (int i = 0; i < output.length; i++) {
      dequantized[i] = (output[i] - zeroPoint) * scale;
    }
    return dequantized;
  }
}

2. Model Pruning

# Prune model to remove unnecessary weights
import tensorflow_model_optimization as tfmot

pruning_params = {
    'pruning_schedule': tfmot.sparsity.keras.PolynomialDecay(
        initial_sparsity=0.0,
        final_sparsity=0.5,
        begin_step=0,
        end_step=1000
    )
}

model_for_pruning = tfmot.sparsity.keras.prune_low_magnitude(
    model, **pruning_params
)

# Fine-tune the pruned model
model_for_pruning.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

model_for_pruning.fit(x_train, y_train, epochs=2)

3. Hardware Acceleration

// Enable GPU acceleration
class AcceleratedInterpreter {
  static Future create(String modelPath) async {
    final options = InterpreterOptions();
    
    // Try to use GPU delegate
    if (Platform.isAndroid) {
      final gpuDelegate = GpuDelegateV2(
        options: GpuDelegateOptionsV2(
          inferencePreference: TfLiteGpuInferenceUsage.fastSingleAnswer,
          inferencePriority1: TfLiteGpuInferencePriority.minLatency,
          inferencePriority2: TfLiteGpuInferencePriority.minMemoryUsage,
          inferencePriority3: TfLiteGpuInferencePriority.maxPrecision,
        ),
      );
      options.addDelegate(gpuDelegate);
    } else if (Platform.isIOS) {
      final metalDelegate = MetalDelegate(
        options: MetalDelegateOptions(
          allowPrecisionLoss: true,
          waitType: TFLMetalDelegateWaitType.passive,
        ),
      );
      options.addDelegate(metalDelegate);
    }
    
    return await Interpreter.fromAsset(
      modelPath,
      options: options,
    );
  }
}

Performance Best Practices

1. Batch Processing

class BatchProcessor {
  final int batchSize;
  final Interpreter interpreter;
  
  BatchProcessor({required this.batchSize, required this.interpreter});
  
  Future>> processBatch(List images) async {
    // Prepare batch input
    final batchInput = List.generate(
      batchSize,
      (i) => i < images.length ? images[i] : Uint8List(0),
    );
    
    // Allocate output buffer
    final output = List.generate(
      batchSize,
      (_) => List.filled(1000, 0.0), // Assuming 1000 classes
    );
    
    // Run batch inference
    interpreter.run(batchInput, output);
    
    // Return only valid results
    return output.take(images.length).toList();
  }
}

2. Memory Management

⚠️ Warning: Always dispose of interpreters and release resources when done to prevent memory leaks.
class ModelManager {
  static final Map _interpreterCache = {};
  
  static Future getInterpreter(String modelPath) async {
    // Check cache first
    if (_interpreterCache.containsKey(modelPath)) {
      return _interpreterCache[modelPath]!;
    }
    
    // Load and cache new interpreter
    final interpreter = await Interpreter.fromAsset(modelPath);
    _interpreterCache[modelPath] = interpreter;
    
    return interpreter;
  }
  
  static void releaseInterpreter(String modelPath) {
    if (_interpreterCache.containsKey(modelPath)) {
      _interpreterCache[modelPath]!.close();
      _interpreterCache.remove(modelPath);
    }
  }
  
  static void releaseAll() {
    _interpreterCache.forEach((_, interpreter) {
      interpreter.close();
    });
    _interpreterCache.clear();
  }
}

3. Threading and Isolates

import 'dart:isolate';

class IsolateInference {
  static Future> runInferenceInIsolate(
    String modelPath,
    Uint8List imageData,
  ) async {
    final receivePort = ReceivePort();
    
    await Isolate.spawn(
      _inferenceIsolate,
      IsolateData(
        modelPath: modelPath,
        imageData: imageData,
        sendPort: receivePort.sendPort,
      ),
    );
    
    return await receivePort.first as List;
  }
  
  static void _inferenceIsolate(IsolateData data) async {
    // Load model in isolate
    final interpreter = await Interpreter.fromAsset(data.modelPath);
    
    // Process image
    final input = _preprocessImage(data.imageData);
    final output = List.filled(1000, 0.0);
    
    // Run inference
    interpreter.run(input, output);
    
    // Clean up
    interpreter.close();
    
    // Send results back
    data.sendPort.send(output);
  }
}

class IsolateData {
  final String modelPath;
  final Uint8List imageData;
  final SendPort sendPort;
  
  IsolateData({
    required this.modelPath,
    required this.imageData,
    required this.sendPort,
  });
}

Advanced Use Cases

1. Style Transfer

class StyleTransfer {
  late Interpreter _interpreter;
  
  Future applyStyle(
    Uint8List contentImage,
    Uint8List styleImage,
  ) async {
    // Preprocess images
    final content = _preprocessImage(contentImage, size: 384);
    final style = _preprocessImage(styleImage, size: 256);
    
    // Prepare output
    final output = List.generate(
      384,
      (_) => List.generate(
        384,
        (_) => List.filled(3, 0.0),
      ),
    );
    
    // Run style transfer
    _interpreter.runForMultipleInputs([content, style], [output]);
    
    // Convert output to image
    return _tensorToImage(output);
  }
}

2. Text Recognition (OCR)

class TextRecognizer {
  late Interpreter _detector;
  late Interpreter _recognizer;
  
  Future> recognizeText(Uint8List imageData) async {
    // Step 1: Detect text regions
    final regions = await _detectTextRegions(imageData);
    
    // Step 2: Recognize text in each region
    final textBlocks = [];
    
    for (final region in regions) {
      final croppedImage = _cropImage(imageData, region);
      final text = await _recognizeTextInRegion(croppedImage);
      
      textBlocks.add(TextBlock(
        text: text,
        boundingBox: region,
        confidence: 0.95,
      ));
    }
    
    return textBlocks;
  }
}

3. Pose Estimation

class PoseEstimator {
  static const List bodyParts = [
    'nose', 'leftEye', 'rightEye', 'leftEar', 'rightEar',
    'leftShoulder', 'rightShoulder', 'leftElbow', 'rightElbow',
    'leftWrist', 'rightWrist', 'leftHip', 'rightHip',
    'leftKnee', 'rightKnee', 'leftAnkle', 'rightAnkle'
  ];
  
  Future> estimatePose(Uint8List imageData) async {
    // Preprocess image
    final input = _preprocessImage(imageData);
    
    // Output: 1 x 17 x 3 (17 keypoints, each with x, y, confidence)
    final output = List.generate(
      17,
      (_) => List.filled(3, 0.0),
    );
    
    // Run inference
    _interpreter.run(input, output);
    
    // Convert to keypoints
    final keypoints = [];
    for (int i = 0; i < 17; i++) {
      keypoints.add(Keypoint(
        part: bodyParts[i],
        x: output[i][0],
        y: output[i][1],
        confidence: output[i][2],
      ));
    }
    
    return keypoints;
  }
}

Production Deployment

Model Versioning

class ModelVersionManager {
  static const String prefsKey = 'model_versions';
  
  static Future downloadAndUpdateModel(
    String modelUrl,
    String modelName,
    String version,
  ) async {
    try {
      // Download model
      final response = await http.get(Uri.parse(modelUrl));
      
      if (response.statusCode == 200) {
        // Save to app documents
        final dir = await getApplicationDocumentsDirectory();
        final file = File('${dir.path}/models/$modelName-$version.tflite');
        
        await file.create(recursive: true);
        await file.writeAsBytes(response.bodyBytes);
        
        // Update version info
        await _updateModelVersion(modelName, version, file.path);
      }
    } catch (e) {
      print('Failed to download model: $e');
      throw Exception('Model download failed');
    }
  }
  
  static Future getModelPath(String modelName) async {
    final prefs = await SharedPreferences.getInstance();
    final versions = prefs.getString(prefsKey);
    
    if (versions != null) {
      final versionMap = json.decode(versions) as Map;
      return versionMap[modelName]?['path'];
    }
    
    return null;
  }
}

Performance Monitoring

class MLPerformanceMonitor {
  static final Map> _inferenceTime = {};
  
  static Future measureInference(
    String modelName,
    Future Function() inference,
  ) async {
    final stopwatch = Stopwatch()..start();
    
    try {
      final result = await inference();
      stopwatch.stop();
      
      // Record inference time
      _inferenceTime[modelName] ??= [];
      _inferenceTime[modelName]!.add(stopwatch.elapsedMilliseconds.toDouble());
      
      // Keep only last 100 measurements
      if (_inferenceTime[modelName]!.length > 100) {
        _inferenceTime[modelName]!.removeAt(0);
      }
      
      return result;
    } catch (e) {
      stopwatch.stop();
      throw e;
    }
  }
  
  static Map getStats() {
    final stats = {};
    
    _inferenceTime.forEach((model, times) {
      if (times.isNotEmpty) {
        final sorted = List.from(times)..sort();
        
        stats[model] = PerformanceStats(
          averageMs: times.reduce((a, b) => a + b) / times.length,
          minMs: sorted.first,
          maxMs: sorted.last,
          p50Ms: sorted[sorted.length ~/ 2],
          p95Ms: sorted[(sorted.length * 0.95).floor()],
          sampleCount: times.length,
        );
      }
    });
    
    return stats;
  }
}

Error Handling and Fallbacks

class RobustMLService {
  Interpreter? _primaryInterpreter;
  Interpreter? _fallbackInterpreter;
  
  Future initialize() async {
    try {
      // Try to load optimized model
      _primaryInterpreter = await Interpreter.fromAsset(
        'models/optimized_model.tflite',
      );
    } catch (e) {
      print('Failed to load optimized model: $e');
    }
    
    // Always load fallback model
    _fallbackInterpreter = await Interpreter.fromAsset(
      'models/fallback_model.tflite',
    );
  }
  
  Future> runInference(Uint8List input) async {
    // Try primary model first
    if (_primaryInterpreter != null) {
      try {
        return await _runWithTimeout(
          () => _executeInference(_primaryInterpreter!, input),
          timeout: Duration(milliseconds: 100),
        );
      } catch (e) {
        print('Primary model failed: $e');
      }
    }
    
    // Fallback to secondary model
    return await _executeInference(_fallbackInterpreter!, input);
  }
  
  Future _runWithTimeout(
    Future Function() function, {
    required Duration timeout,
  }) async {
    return await function().timeout(
      timeout,
      onTimeout: () => throw TimeoutException('Inference timeout'),
    );
  }
}

Conclusion

TensorFlow Lite empowers Flutter developers to build intelligent, responsive mobile applications with on-device machine learning. By following the techniques and best practices outlined in this guide, you can successfully implement everything from simple image classification to complex real-time object detection and custom model deployment.

Key Takeaways

Next Steps

  1. Experiment with Models: Try different pre-trained models from TensorFlow Hub
  2. Train Custom Models: Build models specific to your use case
  3. Optimize Performance: Implement quantization and pruning
  4. Add Edge Features: Combine multiple models for complex scenarios
  5. Monitor Production: Track performance metrics and user experience

About the Author

Utsav Savani is the founder of Innovative Flutter AI and a full-stack product engineer with 8+ years of experience. He designs in Figma, builds in Flutter and ships the backend on Supabase, Firebase and AWS, and leads a 50-person team. Find him on GitHub and LinkedIn.

Work with us

Building something like this?

We design, build and ship Flutter apps with AI features for startups and product teams. Tell us the idea and we will tell you what it takes.

Start a project More articles