Migrating Local Databases in Flutter: From Realm to Drift (and Beyond)
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
So your Flutter app’s local database has become a liability. Maybe it’s crashing randomly, or the package you chose has stopped receiving updates. You’re not alone—many of us have been there. The initial prototype often uses the most convenient database, but as your app grows and you need stability, a migration becomes inevitable.
A common path is moving from an unmaintained or unstable NoSQL solution like Realm to a robust, SQLite-based library such as Drift. This migration isn’t just about swapping packages; it’s a strategic move towards long-term data integrity and reliability.
Let’s break down how to approach this migration methodically.
Step 1: Audit and Understand Your Current Data
Before writing a single line of new code, you need a complete map of your existing data. What objects are stored? What are their relationships? Export a sample of your Realm database and document the schema.
For example, if you had a Session object in Realm:
// Old Realm Model (Conceptual)
class OldSession {
@PrimaryKey()
int? id;
String mantraName;
int count;
DateTime date;
}
Step 2: Design the New Drift Schema
With Drift, you define your schema using Dart (or SQL). Design your new tables, keeping future needs in mind.
Here’s how you might define the equivalent sessions table:
import 'package:drift/drift.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'dart:io';
part 'database.g.dart';
class Sessions extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get mantraName => text()();
IntColumn get count => integer()();
DateTimeColumn get date => dateTime()();
}
@DriftDatabase(tables: [Sessions])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'app_db.sqlite'));
return NativeDatabase(file);
});
}
Step 3: The Migration Script: A Critical Phase
This is the heart of the process. You must write a one-time script that reads all data from the old Realm database and writes it into the new Drift database. Do this during your app’s first launch after the update.
Create a dedicated migration service:
class DatabaseMigrator {
final Realm oldRealmInstance;
final AppDatabase newDriftDatabase;
Future<void> migrateAllData() async {
// 1. Read all old objects
final oldSessions = oldRealmInstance.all<OldSession>();
// 2. Transform and insert into new database
for (final oldSession in oldSessions) {
await newDriftDatabase.into(newDriftDatabase.sessions).insert(
SessionsCompanion.insert(
mantraName: Value(oldSession.mantraName),
count: Value(oldSession.count),
date: Value(oldSession.date),
),
);
}
// 3. Close the old realm
oldRealmInstance.close();
}
}
Crucial: Wrap this in extensive error handling and logging.
Step 4: Update Your App’s Architecture
Your old data layer was tightly coupled to Realm. Now’s the time to abstract it. Implement a repository pattern that uses your new AppDatabase.
abstract class SessionRepository {
Future<List<Session>> getAllSessions();
Future<void> addSession(Session session);
}
class DriftSessionRepository implements SessionRepository {
final AppDatabase db;
DriftSessionRepository(this.db);
@override
Future<List<Session>> getAllSessions() async {
final rows = await db.select(db.sessions).get();
return rows.map((row) => Session.fromRow(row)).toList();
}
// ... other methods
}
Step 5: Planning for the Cloud
Once you have a stable local SQLite foundation, adding cloud sync becomes a more manageable problem.
Final Advice
- Test Relentlessly: Create a comprehensive suite of unit and integration tests for your migration script and new data layer.
- Communicate with Users: For a production app, consider a phased rollout.
- Embrace the Refactor: This migration is a significant investment. Use it as an opportunity to clean up related technical debt in your data layer.
Migrating your database is a complex, high-stakes task, but moving to a well-maintained, standards-based solution like Drift provides the rock-solid foundation your app needs to grow.
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
Optimizing Flutter UI Performance: Best Practices for Date Formatting and Expensive Operations
Developers often face performance bottlenecks when performing expensive operations like date formatting directly within Flutter's `build` method, especially in fast-scrolling lists. This post will delve into common pitfalls, explain why these operations are costly, and provide practical strategies for optimizing UI performance by caching formatters, using `initState`, and leveraging `compute` for background processing without blocking the UI.
Optimizing Your Flutter Dev Setup: IDEs, Simulators, and AI Tools for Peak Productivity
Flutter developers frequently seek to refine their development environments. This post will dive into popular IDE choices like VS Code and Android Studio, discuss best practices for managing iOS and Android simulators (including in-IDE options), and explore the practical integration of AI tools for code generation and problem-solving to boost overall efficiency.
Demystifying Flutter Performance: Practical Strategies for Large-Scale Apps
Flutter's performance is often blamed for issues in complex applications, but the real culprits are usually architectural decisions, inefficient widget rebuilds, and unoptimized resource handling. This post will dive into common performance bottlenecks in large Flutter apps, providing actionable strategies for profiling, optimizing state management, handling images and network requests efficiently, and leveraging CI/CD for continuous performance monitoring.