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
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.
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.
Solving Flutter Web Memory Leaks: A Practical Guide to Identifying and Fixing Performance Issues
Flutter Web applications can suffer from increasing memory usage over time, leading to performance degradation. This post will delve into common causes of memory leaks in Flutter Web, provide practical debugging techniques using browser developer tools and Dart DevTools, and offer actionable strategies to identify and fix these issues for a smoother user experience.