← 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's Hidden Gem: Building Powerful Linux Desktop Apps with Ease

Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease

Flutter is gaining traction as a robust framework for Linux desktop development, offering a smoother experience compared to traditional toolkits like GTK and Qt. This post will explore why Flutter excels for Linux apps, highlight its advantages, and provide practical insights for developers looking to leverage Flutter for their desktop projects.

Cover image for Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows

Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows

Deploying Flutter desktop apps on Windows can be challenging, especially regarding code signing and preventing Windows from blocking installations. This post will guide developers through the process of signing their Flutter desktop applications, exploring alternatives to the Microsoft Store like creating executable installers, and covering the necessary steps to ensure a smooth user experience.

Cover image for Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows

Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows

Developers frequently voice frustrations about Flutter SDK version management and installation processes, particularly on Windows. This post will explore best practices for managing multiple Flutter versions using tools like FVM, discuss integrating Flutter with package managers like Winget for a smoother Windows setup, and offer strategies to ensure a consistent development environment across projects and teams.