Migrating from Isar to Sqflite: A Guide to Robust Local Data Storage in Flutter
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
So your app just got rejected from the Play Store with a cryptic error, or maybe it’s crashing on newer Android devices. You trace the issue back to your local database, Isar. What’s going on? You’re likely facing Android’s new requirement for native libraries to support 16KB memory page sizes, a low-level system change that can break dependencies relying on outdated native binaries. Isar, while once a fantastic performer, hasn’t seen a core update in years and its binaries don’t meet this new requirement, leading to instant rejection or runtime crashes.
The good news? This is a solvable problem by migrating to a battle-tested, actively maintained alternative: Sqflite. It wraps SQLite, a rock-solid database engine that’s ubiquitous and consistently updated for platform changes. Let’s walk through a practical migration.
Understanding the Migration Mindset
Isar is a NoSQL, object-oriented database. Sqflite is a SQL-based relational database. The core task is mapping your data models from objects to tables. This often results in a more explicit and robust data schema.
Step 1: Define Your Schema in SQL
Start by writing the SQL CREATE TABLE statements for your models. Let’s assume you have a simple Task model.
// This was your Isar model (conceptual)
// @Collection()
// class Task {
// Id id = Isar.autoIncrement;
// String title;
// bool isCompleted;
// DateTime dueDate;
// }
// Your new SQL schema
const String createTaskTable = '''
CREATE TABLE tasks(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
is_completed INTEGER NOT NULL CHECK (is_completed IN (0, 1)),
due_date INTEGER
)
''';
Note: SQLite doesn’t have a native bool or DateTime type. We store booleans as integers (0/1) and dates as milliseconds since epoch (INTEGER).
Step 2: Set Up the Database Helper
Create a dedicated class to manage your database connection and initialization.
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
static Database? _database;
factory DatabaseHelper() => _instance;
DatabaseHelper._internal();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'my_app.db');
return await openDatabase(
path,
version: 1,
onCreate: (db, version) async {
await db.execute(createTaskTable);
},
);
}
}
Step 3: Create Your Data Access Object (DAO)
This class will contain all your CRUD operations, translating between Dart objects and database rows.
class TaskDao {
final DatabaseHelper dbHelper = DatabaseHelper();
Future<int> insertTask(Task task) async {
final db = await dbHelper.database;
return await db.insert('tasks', task.toMap());
}
Future<List<Task>> getAllTasks() async {
final db = await dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query('tasks');
return List.generate(maps.length, (i) => Task.fromMap(maps[i]));
}
Future<int> updateTask(Task task) async {
final db = await dbHelper.database;
return await db.update(
'tasks',
task.toMap(),
where: 'id = ?',
whereArgs: [task.id],
);
}
Future<int> deleteTask(int id) async {
final db = await dbHelper.database;
return await db.delete(
'tasks',
where: 'id = ?',
whereArgs: [id],
);
}
}
Step 4: Update Your Model Class
Your model now needs toMap and fromMap methods for serialization.
class Task {
int? id;
String title;
bool isCompleted;
DateTime? dueDate;
Task({
this.id,
required this.title,
required this.isCompleted,
this.dueDate,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'is_completed': isCompleted ? 1 : 0,
'due_date': dueDate?.millisecondsSinceEpoch,
};
}
factory Task.fromMap(Map<String, dynamic> map) {
return Task(
id: map['id'],
title: map['title'],
isCompleted: map['is_completed'] == 1,
dueDate: map['due_date'] != null
? DateTime.fromMillisecondsSinceEpoch(map['due_date'])
: null,
);
}
}
Common Pitfalls and How to Avoid Them
- Forgetting Data Type Conversions: Always remember the SQLite type limitations. Booleans and dates are the most common trip-ups. Be consistent with your
toMap/fromMaplogic. - Blocking the UI Thread: Sqflite operations are asynchronous, but complex queries on large datasets can still cause jank. Use
computefor intensive operations or consider pagination (LIMITandOFFSETin your queries). - Missing Database Indexes: For query performance on large tables (e.g.,
WHERE due_date > ?), remember to create indexes. You can add these in youronCreateoronUpgrademethods:await db.execute('CREATE INDEX idx_due_date ON tasks(due_date)'); - Handling Database Versioning: When you add a new table or column in an app update, you must implement
onUpgrade. Increment theversionparameter inopenDatabaseand write migration logic. Never just drop and recreate the table in production, as users will lose their data.
Final Thoughts
Migrating from Isar to Sqflite is more than a package swap—it’s a shift from an object store to a relational schema. While it requires more upfront SQL knowledge, the payoff is immense: stability, future-proofing against OS changes, and the power of SQL queries. Start by migrating a single, non-critical model to test the pattern. Once you’re comfortable, you can systematically move your data, ensuring your app remains reliable for all your users, no matter what Android version they’re on.
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
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.
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.
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.