← Back to posts Cover image for Running LLMs On-Device in Flutter: A Practical Guide to Local AI Integration

Running LLMs On-Device in Flutter: A Practical Guide to Local AI Integration

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

Why Run LLMs On-Device?

Imagine offering smart, conversational features in your Flutter app that work instantly—on a plane, in a remote area, or simply for users wary of their data leaving the phone. This is the promise of on-device Large Language Models (LLMs). By moving inference from the cloud to the user’s device, you unlock offline functionality, guarantee user privacy, and eliminate recurring API costs. The trade-offs? Increased app size, higher memory usage, and the need for thoughtful performance management. Let’s explore how to navigate this landscape practically.

The On-Device LLM Toolbox for Flutter

The Flutter ecosystem offers several pathways for local inference. Your choice depends on the model format and the level of abstraction you need.

1. The TensorFlow Lite (TFLite) Route For models converted to TensorFlow Lite format, the tflite_flutter package is the go-to choice. It provides a robust, low-level interface.

First, add the dependency to your pubspec.yaml:

dependencies:
  tflite_flutter: ^0.10.0

Here’s a basic setup to load a TFLite model and run inference. Assume we have a simple text generation model named local_llm.tflite and its associated vocabulary file in the assets folder.

import 'package:tflite_flutter/tflite_flutter.dart';

class LocalTextGenerator {
  late Interpreter _interpreter;
  late List<String> _vocabulary;

  Future<void> initialize() async {
    // Load the model
    final options = InterpreterOptions();
    _interpreter = await Interpreter.fromAsset('local_llm.tflite', options: options);

    // Load vocabulary (simplified example)
    String vocabString = await rootBundle.loadString('assets/vocab.txt');
    _vocabulary = vocabString.split('\n');

    // Print model input/output details (crucial for debugging)
    print('Input Tensors: ${_interpreter.getInputTensors()}');
    print('Output Tensors: ${_interpreter.getOutputTensors()}');
  }

  String generate(String prompt) {
    // 1. Tokenize: Convert the prompt string to model input indices.
    //    This is a simplified placeholder. Real tokenization is model-specific.
    List<int> tokenizedInput = _tokenizePrompt(prompt);

    // 2. Prepare input/output buffers. Shapes depend on your model.
    var input = [tokenizedInput];
    var output = List.filled(1, List.filled(100, 0)); // Example shape

    // 3. Run inference
    _interpreter.run(input, output);

    // 4. Decode: Convert output indices back to text.
    return _decodeOutput(output[0]);
  }

  // Placeholder for actual tokenization logic.
  List<int> _tokenizePrompt(String prompt) {
    return prompt.toLowerCase().split(' ').map((word) {
      int index = _vocabulary.indexOf(word);
      return index != -1 ? index : 0; // 0 as unknown token
    }).toList();
  }

  // Placeholder for actual decoding logic.
  String _decodeOutput(List<int> indices) {
    return indices.map((idx) => idx < _vocabulary.length ? _vocabulary[idx] : '').join(' ');
  }

  void dispose() {
    _interpreter.close();
  }
}

2. Leveraging Higher-Level Runtimes For popular model formats like GGUF (used by llama.cpp) or ONNX, you’ll need to use platform-specific libraries bridged via Flutter’s Foreign Function Interface (FFI) or method channels.

Integrating an LLM isn’t free. Here’s what to expect and how to mitigate the costs.

  • App Size: A model file is the single biggest contributor. A small 100MB model will directly add to your download size. Solution: Use Flutter’s dynamic delivery (App Bundles on Android, app thinning on iOS) or offer the model as an optional post-install download.
  • Memory & CPU: Inference is computationally heavy. Loading a model consumes RAM, and generation heats up the CPU. Solution: Choose smaller, quantized models (e.g., 3B parameter models or smaller, in INT4/INT8 format). Always run inference in an isolate to keep your UI jank-free.
  • Battery: Sustained CPU load drains batteries. Solution: Implement smart caching of responses, limit the maximum generation length, and provide users with settings to control AI feature usage.

A Practical Implementation Pattern

Here’s a more complete pattern using an isolate to avoid freezing the UI, a critical best practice.

import 'dart:isolate';

class IsolatedLLMWorker {
  SendPort? _sendPort;
  final ReceivePort _receivePort = ReceivePort();
  Isolate? _isolate;

  Future<void> start() async {
    _isolate = await Isolate.spawn(_isolateEntry, _receivePort.sendPort);
    _sendPort = await _receivePort.first as SendPort;
  }

  Future<String> prompt(String message) async {
    if (_sendPort == null) throw Exception('Worker not started');
    final responsePort = ReceivePort();
    _sendPort!.send([message, responsePort.sendPort]);
    return await responsePort.first as String;
  }

  void dispose() {
    _receivePort.close();
    _isolate?.kill(priority: Isolate.immediate);
  }

  // This runs in the background isolate
  static void _isolateEntry(SendPort mainSendPort) {
    final receivePort = ReceivePort();
    mainSendPort.send(receivePort.sendPort); // Send our port back to main

    LocalTextGenerator? generator; // Your inference class from earlier

    receivePort.listen((message) async {
      String prompt = message[0];
      SendPort replyPort = message[1];

      generator ??= await LocalTextGenerator().initialize();
      String result = generator!.generate(prompt);
      replyPort.send(result);
    });
  }
}

// Usage in your widget
void _askQuestion() async {
  setState(() => _isLoading = true);
  final answer = await _llmWorker.prompt(_questionController.text);
  setState(() {
    _answer = answer;
    _isLoading = false;
  });
}

Choosing Your Model and Next Steps

Start with a small, quantized model like TinyLlama or Phi-2 to validate your use case. The flutter_rust_bridge is a powerful tool if you need to integrate a native runtime like llama.cpp written in Rust.

Remember, on-device LLMs won’t match the breadth of GPT-4, but they excel at specific, well-defined tasks. Test rigorously on your target hardware—especially lower-end devices. The field is moving fast, so what’s challenging today may be trivial tomorrow. By integrating local AI now, you’re building for a faster, more private, and more resilient future for your apps.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter apps are often criticized for their larger-than-native binary sizes. This post will dive into practical, actionable strategies to significantly reduce your Flutter app's footprint, including build configurations, asset optimization, and dependency management, demonstrating how to achieve app sizes under 10MB.

Cover image for Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Implementing in-app subscriptions in Flutter can be surprisingly complex, especially when dealing with cross-platform nuances, receipt validation, and managing different store configurations. This post will delve into the common challenges developers face, compare solutions like RevenueCat with custom implementations, and provide best practices for setting up and maintaining a robust subscription system.

Cover image for Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Android emulators are notorious for hogging RAM, often pushing developer machines into swap and slowing down Flutter development. This post will explore the reasons behind high emulator memory consumption and provide practical, actionable strategies and tools (including a custom-built solution) to significantly reduce RAM usage, ensuring a smoother development experience without sacrificing functionality.