
A Deep Dive into Isar DB
Our production Flutter app was grinding to a halt. What started as smooth scrolling through a few hundred records had devolved into stuttering lists and frustrated users as our dataset grew to tens of thousands of entries. We were using Hive, and while it had served us well initially, the performance bottlenecks became impossible to ignore. Eventually we reached a breaking point, as even the most menial of tasks became a really painful experience for our users. We needed a solution, and we needed it fast.
That's when we discovered Isar. The migration took sometime, but the results were worthwile. Queries that took seconds now completed in milliseconds. Complex filtering operations that made users wait became instantaneous. Our app felt new again.
In this article, I'll walk you through why we made the switch, how Isar compares to other Flutter persistence options, and the patterns we've learned from running it in production. If you're hitting performance walls with your current database solution, this might be exactly what you need.
What is Isar?
Isar is a lightning fast, NoSQL database specifically built for Flutter and Dart. Released in 2021, it's written in Rust with Dart bindings, offering native performance while maintaining a clean, idiomatic Dart API. Unlike other Flutter databases that are ports from other platforms, Isar was designed from the ground up for mobile development.
The key distinguishing features:
- Blazing fast: Consistently outperforms SQLite, Hive, and ObjectBox in benchmarks
- Type safe: Fully typed queries with code generation
- Synchronous and asynchronous: Choose the right API for your use case
- ACID compliant: Full transaction support with rollback capabilities
- Multi isolate support: Safe concurrent access across isolates
- Zero configuration: No boilerplate setup required
The Flutter Persistence Landscape
Before diving into Isar, let's understand the current options:
SharedPreferences
Best for: Simple key value storage
final prefs = await SharedPreferences.getInstance();await prefs.setString('username', 'john_doe');Limitations: No complex queries, no relations, limited data types, performance degrades with size.
Hive
Best for: Fast, simple NoSQL storage
final box = await Hive.openBox('users');box.put('user1', User(name: 'John', age: 30));Limitations: Manual indexing, weaker query capabilities, no built in relations.
SQLite (via sqflite)
Best for: Complex relational data
final db = await openDatabase('my_db.db');await db.insert('users', {'name': 'John', 'age': 30});Limitations: Raw SQL strings (error prone), manual serialization, synchronous only.
Drift (formerly Moor)
Best for: Type safe SQL
// Using Drift's query builderfinal users = await (select(users) ..where((u) => u.age.isBiggerOrEqualValue(18))).get();Limitations: SQL based (steeper learning curve), more boilerplate, slower than NoSQL.
Why Isar Stands Out
Isar combines the best aspects of these solutions while avoiding their pitfalls:
Performance That Matters
Real world benchmark results (inserting 10,000 records):
- Isar: ~150ms
- Hive: ~280ms
- SQLite: ~450ms
- ObjectBox: ~200ms
Query performance (filtering 10,000 records):
- Isar: ~8ms
- Hive: ~45ms
- SQLite: ~60ms
Type Safety Without Compromise
Unlike SQLite's string based queries or Hive's dynamic typing, Isar provides full type safety:
// Type safe queries - compiler catches errorsfinal adults = await isar.users .filter() .ageGreaterThan(18) .findAll();
// Compare to SQLite:// await db.rawQuery('SELECT * FROM users WHERE age > ?', [18]);// (No compile time checking, easy to make mistakes)Getting Started with Isar
Installation
dependencies: isar: ^3.1.0 isar_flutter_libs: ^3.1.0
dev_dependencies: isar_generator: ^3.1.0 build_runner: ^2.4.0Defining Models
Isar uses code generation for maximum performance. Here's a complete example:
import 'package:isar/isar.dart';
part 'user.g.dart';
@collectionclass User { Id id = Isar.autoIncrement;
@Index() late String name;
late int age;
@Index(type: IndexType.value) late String email;
// Embedded objects late Address? address;
// Lists late List<String> tags;}
@embeddedclass Address { late String street; late String city; late String country;}Opening a Database
final isar = await Isar.open([ UserSchema, ProductSchema, OrderSchema,]);Advanced Isar Features
Powerful Queries
Isar's query builder is intuitive and performant:
// Complex filteringfinal results = await isar.users .filter() .ageGreaterThan(18) .and() .emailContains('@gmail.com') .or() .tagsElementEqualTo('premium') .sortByName() .limit(50) .findAll();
// Full text searchfinal searchResults = await isar.users .filter() .nameMatches('*john*', caseSensitive: false) .findAll();
// Aggregationsfinal avgAge = await isar.users .filter() .ageGreaterThan(0) .ageAverage();Links and Relations
Isar makes relations easy without foreign keys:
@collectionclass Author { Id id = Isar.autoIncrement; late String name; // One to many relationship final books = IsarLinks<Book>();}
@collectionclass Book { Id id = Isar.autoIncrement; late String title; // Many to one (backreference) final author = IsarLink<Author>();}
// Using relationsfinal author = Author()..name = 'John Doe';final book = Book()..title = 'Flutter Guide';
await isar.writeTxn(() async { await isar.authors.put(author); author.books.add(book); await author.books.save();});
// Query across relationsfinal authorsWithBooks = await isar.authors .filter() .books((q) => q.titleContains('Flutter')) .findAll();Transactions
ACID compliant transactions ensure data integrity:
// Write transactionawait isar.writeTxn(() async { final user = User() ..name = 'John' ..age = 30; await isar.users.put(user); // If any operation fails, entire transaction rolls back if (user.age < 18) { throw Exception('User must be adult'); } await isar.settings.put(Settings()..lastUser = user.id);});
// Read transactions (faster, no locking)final users = await isar.txn(() async { return await isar.users.where().findAll();});Watchers and Reactive Queries
Isar provides real time updates:
// Watch a single objectfinal userStream = isar.users.watchObject(userId);userStream.listen((user) { print('User updated: ${user?.name}');});
// Watch query resultsfinal adultsStream = isar.users .filter() .ageGreaterThan(18) .watch(fireImmediately: true);
// Use in StreamBuilderStreamBuilder<List<User>>( stream: adultsStream, builder: (context, snapshot) { if (!snapshot.hasData) return CircularProgressIndicator(); return ListView.builder( itemCount: snapshot.data!.length, itemBuilder: (context, index) { final user = snapshot.data![index]; return ListTile(title: Text(user.name)); }, ); },);Indexes for Performance
Strategic indexing dramatically improves query speed:
@collectionclass Product { Id id = Isar.autoIncrement;
// Simple index (faster equality checks) @Index() late String sku;
// Composite index (queries on category + price) @Index(composite: [CompositeIndex('price')]) late String category; late double price;
// Unique index (enforces uniqueness) @Index(unique: true) late String barcode;
// Hash index (fastest for equality, no range queries) @Index(type: IndexType.hash) late String internalCode;}
// Queries that use indexesfinal product = await isar.products .filter() .skuEqualTo('ABC123') // Uses sku index .findFirst();
final electronics = await isar.products .filter() .categoryEqualTo('Electronics') // Uses composite index .and() .priceLessThan(1000) .findAll();Migration Strategies
Isar handles schema changes gracefully:
// Version 1@collectionclass User { Id id = Isar.autoIncrement; late String name;}
// Version 2: Adding field (automatic)@collectionclass User { Id id = Isar.autoIncrement; late String name; String? email; // New nullable field}
// Version 3: Data migration neededFuture<void> migrateToV3(Isar isar) async { final oldUsers = await isar.users.where().findAll(); await isar.writeTxn(() async { for (var user in oldUsers) { // Migrate data user.email ??= '${user.name.toLowerCase()}@example.com'; await isar.users.put(user); } });}Performance Optimization Patterns
Batch Operations
// Inefficient: Multiple transactionsfor (var user in users) { await isar.writeTxn(() async { await isar.users.put(user); });}
// Efficient: Single transactionawait isar.writeTxn(() async { await isar.users.putAll(users);});Lazy Loading
// Load only what you needfinal userIds = await isar.users .where() .idProperty() .findAll();
// Load full objects on demandfinal user = await isar.users.get(userIds.first);Using Synchronous API
When performance is critical and you're not on the main isolate:
// Async (typical usage)final users = await isar.users.where().findAll();
// Sync (faster, but blocks thread)final users = isar.users.where().findAllSync();Real World Use Cases
Offline First App with Sync
class DataRepository { final Isar isar; DataRepository(this.isar); // Save data locally Future<void> saveArticle(Article article) async { await isar.writeTxn(() async { article.syncStatus = SyncStatus.pending; await isar.articles.put(article); }); _syncToServer(); } // Sync pending changes Future<void> _syncToServer() async { final pending = await isar.articles .filter() .syncStatusEqualTo(SyncStatus.pending) .findAll(); for (var article in pending) { try { await api.syncArticle(article); await isar.writeTxn(() async { article.syncStatus = SyncStatus.synced; await isar.articles.put(article); }); } catch (e) { // Handle sync errors } } }}Implementing Full Text Search
@collectionclass Note { Id id = Isar.autoIncrement;
@Index(type: IndexType.value, caseSensitive: false) late String title;
@Index(type: IndexType.value, caseSensitive: false) late String content;
late DateTime createdAt;}
// Search implementationFuture<List<Note>> searchNotes(String query) async { final words = query.toLowerCase().split(' '); return await isar.notes .filter() .anyOf( words, (q, word) => q .titleContains(word, caseSensitive: false) .or() .contentContains(word, caseSensitive: false), ) .sortByCreatedAtDesc() .findAll();}Isar vs The Competition
When to Use Isar
- High performance requirements (thousands of records)
- Complex queries needed
- Type safety is important
- Multi isolate access required
- NoSQL flexibility preferred
When to Use SQLite
- Team has strong SQL expertise
- Need raw SQL query control
- Existing SQL schema to migrate
- Simpler, well understood technology
When to Use Hive
- Extremely simple use case
- Minimal dependencies preferred
- Very small data sets
- Learning curve is primary concern
When to Use Drift
- Strong preference for SQL
- Need migration tooling
- Complex joins and relations
- Type safety with SQL
Common Pitfalls and Solutions
Forgetting to Run Code Generation
# Always run after model changesflutter pub run build_runner buildNot Using Indexes
// Slow: No index@collectionclass User { late String email;}
// Fast: With index@collectionclass User { @Index() late String email;}Keeping Database Open
// Open once, use everywhereclass Database { static Isar? _instance; static Future<Isar> get instance async { if (_instance != null) return _instance!; _instance = await Isar.open([UserSchema]); return _instance!; }}Testing Strategies
void main() { late Isar isar;
setUp(() async { // Use in-memory database for tests isar = await Isar.openInMemory([UserSchema]); });
tearDown(() async { await isar.close(deleteFromDisk: true); });
test('should save and retrieve user', () async { final user = User() ..name = 'John' ..age = 30;
await isar.writeTxn(() async { await isar.users.put(user); });
final retrieved = await isar.users.get(user.id); expect(retrieved?.name, 'John'); });}Conclusion
Isar represents a significant leap forward in Flutter data persistence. Its combination of raw performance, type safety, and developer friendly API makes it an excellent choice for most Flutter applications that need local storage beyond simple key value pairs.
The decision matrix is straightforward:
- Simple key value needs: SharedPreferences
- Small datasets with simple queries: Hive
- SQL expertise and complex joins: Drift
- High performance NoSQL with complex queries: Isar
After building several production apps with Isar, I can confidently say it delivers on its promises. The performance gains are real, the API is intuitive, and the type safety catches bugs before they reach production.
If you're starting a new Flutter project or considering migrating from another database solution, give Isar a serious look. The investment in learning its patterns pays dividends in performance and maintainability.
Further Resources:
- Isar Documentation
- Isar GitHub Repository
- Performance Benchmarks
- Isar Inspector - Database debugging tool
Try it yourself: Clone the Isar samples repo to see real world implementations and best practices.