← Back to posts Cover image for Choosing the Right Local Database for Flutter: Beyond Isar's End-of-Life

Choosing the Right Local Database for Flutter: Beyond Isar's End-of-Life

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

So, you built your Flutter app with a snappy local database, your data layer is humming along, and then you get the news: your chosen database, Isar, is reaching its end-of-life. This scenario has left many developers in a tough spot, facing the non-trivial task of migrating their app’s foundational data layer. The good news? The Flutter ecosystem is robust, and there are excellent, well-maintained alternatives ready for you. Let’s move beyond the panic and evaluate two of the strongest contenders: ObjectBox and Drift.

The core lesson here is about sustainability. Choosing a database is a long-term architectural decision. While features and benchmarks are important, the health of the project—active maintenance, a clear roadmap, and community support—is paramount. Your new choice needs to be a dependable foundation for years to come.

Contender 1: ObjectBox – High-Performance NoSQL

ObjectBox is a superfast, NoSQL object database built for high performance on devices. If you loved Isar’s NoSQL approach and its speed, ObjectBox will feel familiar and is arguably its spiritual successor in the Flutter space. It’s maintained by a dedicated company, which bodes well for its future.

Key Features:

  • Blazing Speed: Benchmarks often show it outperforming SQLite for many operations.
  • Object-Oriented: You work directly with your Dart objects; no ORM mapping needed.
  • Live Queries: Get reactive streams that automatically update when the underlying data changes.
  • Strong Relations: Supports To-One, To-Many, and even Many-to-Many relationships.

Integration & Code Example:

First, add the dependencies to your pubspec.yaml:

dependencies:
  objectbox: ^1.8.1
  objectbox_flutter_libs: ^1.8.1

dev_dependencies:
  objectbox_generator: ^1.8.1
  build_runner: ^2.4.0

Define your entity. Notice the @Entity() annotation and the obligatory id field.

// customer.dart
import 'package:objectbox/objectbox.dart';

@Entity()
class Customer {
  int id = 0; // ObjectBox requires an int id field (0 for new objects).
  String name;
  String email;

  Customer({this.id = 0, required this.name, required this.email});
}

Run the code generator to create the necessary binding code:

flutter pub run build_runner build

Using the database is straightforward:

// main.dart (simplified snippet)
import 'package:objectbox/objectbox.dart';

void main() async {
  final store = await openStore(); // Opens the ObjectBox store.
  final customerBox = store.box<Customer>();

  // Put (Insert/Update)
  final newCustomer = Customer(name: 'Alice Smith', email: 'alice@example.com');
  final customerId = customerBox.put(newCustomer);
  print('Saved customer with ID: $customerId');

  // Query
  final query = customerBox.query(Customer_.name.equals('Alice Smith')).build();
  final results = query.find();
  query.close();

  // Live Query (reactive stream)
  final liveQuery = customerBox.query().watch();
  liveQuery.stream.listen((query) {
    print('Customers updated: ${query.find()}');
  });
}

Contender 2: Drift – The Power of SQL, Simplified

Drift (formerly Moor) is a reactive persistence library built on SQLite. It lets you write queries in both typesafe Dart and traditional SQL, giving you the full power and battle-tested reliability of SQLite with a modern, fluent API.

Key Features:

  • SQLite Power: Leverages one of the most deployed databases in the world.
  • Compile-Time Safety: Queries are checked for errors at compile time, not at runtime.
  • Reactive by Design: Easily convert queries into Streams that update when data changes.
  • Flexible: Write pure SQL or use a fluent Dart API.

Integration & Code Example:

Add Drift and its generator to your pubspec.yaml:

dependencies:
  drift: ^2.19.0
  sqlite3_flutter_libs: ^0.5.0
  path_provider: ^2.1.0

dev_dependencies:
  drift_dev: ^2.19.0
  build_runner: ^2.4.0

Define your tables and data access objects:

// database.dart
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
part 'database.g.dart'; // This will be generated

class TaskItems extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text().withLength(min: 1, max: 100)();
  BoolColumn get isCompleted => boolean().withDefault(const Constant(false))();
}

@DriftDatabase(tables: [TaskItems])
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(_openConnection());
  @override
  int get schemaVersion => 1;
}

LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File('${dbFolder.path}/my_app_db.sqlite');
    return NativeDatabase(file);
  });
}

Generate the code with flutter pub run build_runner build. Then, use it in your app:

// Using the database
final db = AppDatabase();

// Insert a task
await db.into(db.taskItems).insert(
      TaskItemsCompanion.insert(title: 'Migrate database'),
    );

// Query all incomplete tasks (reactive stream)
Stream<List<TaskItem>> watchIncompleteTasks() {
  return (db.select(db.taskItems)
        ..where((t) => t.isCompleted.equals(false)))
      .watch();
}

// Use raw SQL for complex operations
Future<void> archiveOldTasks() async {
  await db.customStatement('''
    UPDATE task_items SET title = '[ARCHIVED] ' || title 
    WHERE is_completed = 1 AND date_created < ?
  ''', [DateTime.now().subtract(Duration(days: 30))]);
}

How to Choose?

  • Choose ObjectBox if: Your app demands maximum read/write performance, you prefer a NoSQL/Object-oriented model, and you need built-in reactive live queries without extra setup. It’s excellent for data-heavy, performance-critical applications.
  • Choose Drift if: You value the stability and universality of SQLite, need to execute complex queries or migrations, or your team already has strong SQL knowledge. It offers unparalleled flexibility and control.

Common Migration Mistake to Avoid: Don’t try to write a one-shot converter that moves all data from Isar to your new database on the first run. This can block the UI and cause timeouts. Instead, design a gradual migration strategy. Initialize your new database, and lazily move old data as it’s accessed, or run the migration in a background isolate, showing a progress indicator to the user.

Ultimately, both ObjectBox and Drift are fantastic, production-ready choices backed by active maintenance. Your decision hinges on your app’s specific needs and your team’s preference for NoSQL versus SQL.

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

Related Posts

Cover image for Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations

Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations

Many Flutter apps need to display content that changes based on user locale, but also comes from a backend (like Firebase). This post will explore best practices for fetching and integrating dynamic, localized content from a backend, ensuring a seamless user experience across different languages and regions without hardcoding translations.

Cover image for Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues

Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues

Developers frequently encounter cryptic type mismatch errors like 'The argument type X can't be assigned to the parameter type Y' or '_InternalLinkedHashMap has no instance method 'cast''. This post will demystify these common Flutter/Dart type errors, explain their root causes (e.g., conflicting imports, dynamic typing pitfalls, JSON deserialization issues), and provide practical solutions to diagnose and fix them, improving code robustness and reducing debugging time.