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
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.