Skip to content

Persistence

Amayyas edited this page Jul 27, 2026 · 1 revision

Persistence

The SDK can save and restore conversations across app restarts through the Memory interface. The core package ships two dependency-free implementations and stays free of storage dependencies (no Hive, SQLite, or shared_preferences) — you plug in whichever backend fits your app.

Overview

  • Memory — the storage interface: saveConversation, loadConversation, deleteConversation, listConversationIds, listSummaries, clearAll.
  • InMemoryMemory — stores conversations as JSON snapshots in memory. Good for tests and temporary sessions; lost on restart.
  • LimitedMemory — a decorator that caps another Memory to the N most recently used conversations, evicting the oldest.
  • JsonFileMemory — one JSON file per conversation on disk (mobile and desktop only — see Web support below).
  • ConversationSummary — lightweight metadata (id, title, messageCount, createdAt, updatedAt) for building a conversation-list UI without loading full message histories.

Quick Start

final ai = FlutterAI(
  provider: AIProvider.anthropic,
  config: AIConfig(apiKey: 'sk-ant-...'),
);

final memory = InMemoryMemory();

// Save the conversation automatically after every turn.
await ai.attachMemory(memory);
await ai.chat('Hello!');

// ... later, e.g. after an app restart with a fresh FlutterAI instance ...
final restored = await ai.loadConversation(memory, savedConversationId);
if (restored) {
  print('Resumed: ${ai.conversation.title}');
}

API

Auto-save with attachMemory

attachMemory saves the conversation immediately, then again after every subsequent update (message added/removed, cleared, truncated) — driven by the same update stream ContextManager.updates already exposes. Saves are fire-and-forget (not awaited by chat()/streamChat()), so a slow storage backend never adds latency to a request.

await ai.attachMemory(memory);
// ... chat normally; every turn is persisted in the background ...
await ai.detachMemory(); // stop auto-saving

Attaching a new memory replaces the previous one — only one memory can be attached for auto-save at a time.

One-shot save

await ai.saveConversation(memory); // saves the current conversation once

Loading a conversation

final restored = await ai.loadConversation(memory, id);
// Returns false and leaves the current conversation untouched if `id`
// isn't found in `memory`.

Listing saved conversations

final summaries = await memory.listSummaries(); // newest updatedAt first
for (final s in summaries) {
  print('${s.title ?? s.id}: ${s.messageCount} messages');
}

Capping storage with LimitedMemory

final memory = LimitedMemory(
  delegate: InMemoryMemory(), // or JsonFileMemory, or your own
  maxConversations: 50,
);

File-based storage (mobile / desktop)

import 'package:path_provider/path_provider.dart';

final dir = await getApplicationDocumentsDirectory();
final memory = JsonFileMemory(
  directoryPath: '${dir.path}/conversations',
);

path_provider is not a dependency of this SDK — add it to your own app if you want a standard, per-platform documents directory.

Web support

dart:io isn't available on the web, so JsonFileMemory resolves to a stub there that throws UnsupportedError immediately on construction. Use InMemoryMemory, or implement Memory on top of a web storage API (IndexedDB, localStorage) — see below.

Implementing your own Memory

Implement the four persistence methods plus listSummaries (or derive it from listConversationIds + loadConversation, as JsonFileMemory does) against any backend — Hive, SQLite (sqflite/drift), shared_preferences, a REST API, IndexedDB on the web, etc.

class HiveMemory implements Memory {
  HiveMemory(this._box);
  final Box<Map> _box;

  @override
  Future<void> saveConversation(Conversation conversation) async {
    await _box.put(conversation.id, conversation.toJson());
  }

  @override
  Future<Conversation?> loadConversation(String id) async {
    final json = _box.get(id);
    return json != null ? Conversation.fromJson(json.cast<String, dynamic>()) : null;
  }

  @override
  Future<bool> deleteConversation(String id) async {
    if (!_box.containsKey(id)) return false;
    await _box.delete(id);
    return true;
  }

  @override
  Future<List<String>> listConversationIds() async => _box.keys.cast<String>().toList();

  @override
  Future<List<ConversationSummary>> listSummaries() async => [
        for (final json in _box.values)
          ConversationSummary.fromConversation(
            Conversation.fromJson(json.cast<String, dynamic>()),
          ),
      ]..sort((a, b) => b.updatedAt.compareTo(a.updatedAt));

  @override
  Future<void> clearAll() => _box.clear();
}

Conversation.toJson() / Conversation.fromJson() already handle every content type (text, images, audio, documents, tool calls/results), so any backend that can store a Map<String, dynamic> (or its JSON-encoded string form) works.

Organizing conversations with IndexedConversation

For apps that need tags, pinning, or an AI-generated summary alongside the conversation, wrap it in IndexedConversation:

final indexed = IndexedConversation(
  conversation: ai.conversation,
  tags: ['travel', 'planning'],
  pinned: true,
);

final json = indexed.toJson(); // conversation + tags + summary + pinned + archived

IndexedConversation doesn't have its own Memory implementation — store its toJson()/fromJson() output the same way you would a plain Conversation.

Clone this wiki locally