Flutter Supabaseオフラインファースト:Drift・PowerSync同期

ネットワーク接続が不安定な地下鉄やエレベーター、電波の届きにくい場所でモバイルアプリが「ネットワークエラー」のスピナーを表示したままフリーズしてしまうと、ユーザーはすぐにアプリを削除してしまいます。従来の「ネットワークリクエスト ➔ 失敗時にキャッシュ読み込み」という構造では、頻繁なローディングスピナーや古いデータ(Stale State)の露出を避けることができません。
2026年のモバイルアプリ開発における世界標準は、**オフラインファースト(Offline-First / Local-First)アーキテクチャです。オフラインファーストアプリでは、ブラウザやデバイス内部のローカルSQLiteデータベースが単一の真実のソース(Single Source of Truth)**となり、UIはネットワーク状態を意識することなく即座に反応します。
本記事では、FlutterとSupabase(PostgreSQL)環境においてDrift SQLite、PowerSync同期エンジン、そしてRiverpod 3.0を組み合わせ、オフライン応答性とリアルタイムサーバー同期を100%保証するエンタープライズオフラインファーストアーキテクチャを完璧に構築します。
要約
- 単一の真実のソース(Single Source of Truth): UIはSupabase APIを直接呼び出さず、ローカルDrift SQLiteデータベースのみを購読します。データ変更時には0msの応答性を保証します。
- PowerSyncエンジンの役割: ローカルSQLiteとSupabase Postgresの間で、**部分同期(Partial Sync)、衝突解決(Conflict Resolution)、非同期アウトボックス(Outbox)**処理を完全に自動化します。
- Riverpod 3.0の結合:
streamProviderを介してローカルSQLiteのバインディングストリームを購読することで、オフライン状態でもUIが即座に更新されます。- ネットワークの透明性: 地下鉄などでWi-Fiが切断されても、ユーザーはデータの書き込み・修正を継続でき、再接続時にバックグラウンドでバックエンドへ自動同期されます。
- 旧式アーキテクチャ(Brick等)からの脱却: 単純なHTTPキャッシュや過度なラッパーライブラリの代わりに、2026年の標準であるDrift + PowerSyncの組み合わせにより、レンダリングラグとデータ損失を100%防止します。
1. オフラインファーストパラダイム:ネットワークキャッシュ vs ローカルファースト
従来のキャッシュ方式と2026年のオフラインファーストアーキテクチャの決定的な違いです。
[従来のネットワークキャッシュ方式] ❌
UI ──► Network Fetch (300ms スピナー) ──(失敗時)──► ローカルキャッシュ読み込み (古いデータ)
[2026 オフラインファーストアーキテクチャ] ⭕️
UI ──(0ms 即時読み込み/書き込み)──► [Drift Local SQLite (Single Source of Truth)]
▲
│ (バックグラウンド同期 0ms〜ネットワーク接続時)
▼
[PowerSync Engine]
▲
│ (Real-time Sync)
▼
[Supabase PostgreSQL]
オフラインファースト構造では、ネットワーク接続が切断されてもアプリの動作に一切の制約がありません。すべてのデータ読み込み・書き込みはデバイス内部のDrift SQLiteにより即座に行われ、UIは0msで反応します。そしてPowerSyncエンジンがバックグラウンドのアウトボックスキュー(Outbox Queue)に変更事項を記録しておき、ネットワークが復旧し次第、即座にSupabaseへ同期します。
2. Drift SQLite + PowerSync + Riverpod 3.0 アーキテクチャレイヤー
| レイヤー | 役割および構成要素 | 主要な特徴 |
|---|---|---|
| UI & State Layer | Flutter UI + Riverpod 3.0 (NotifierProvider) |
ネットワーク状態に関係なくローカルDrift DBのStreamのみを購読 |
| Local Persistence | Drift (SQLite ORM) | 単一の真実のソース。型安全性が優れたDart SQLiteデータベース |
| Sync Engine | PowerSync Client | ローカルSQLite ↔ Supabase Postgres間のリアルタイム差分同期 & 衝突解決 |
| Remote Database | Supabase (PostgreSQL) | RLS(Row Level Security)ベースのマルチテナントデータ保管庫 |
3. オフラインファースト実践実装コード
ステップ1:Drift SQLiteテーブル定義(lib/database/app_database.dart)
Drift公式ドキュメントの規格に合わせた型安全性のあるローカルDB定義です。
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
part 'app_database.g.dart';
class Todos extends Table {
TextColumn get id => text()();
TextColumn get title => text()();
BoolColumn get isCompleted => boolean().withDefault(const Constant(false))();
DateTimeColumn get updatedAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}
@DriftDatabase(tables: [Todos])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(NativeDatabase.memory());
@override
int get schemaVersion => 1;
// ローカルDBの変更事項を0msストリームとして公開
Stream<List<Todo>> watchAllTodos() {
return (select(todos)..orderBy([(t) => OrderingTerm.desc(t.updatedAt)])).watch();
}
}
ステップ2:PowerSyncオフラインスキーマ & 衝突解決設定(lib/sync/powersync.dart)
PowerSync公式ドキュメントを通じたSupabaseオフラインバックグラウンド同期コネクタの設定です。
import 'package:powersync/powersync.dart';
final schema = Schema([
Table('todos', [
Column.text('title'),
Column.integer('is_completed'),
Column.text('updated_at'),
])
]);
late final PowerSyncDatabase db;
Future<void> initPowerSync(String supabaseUrl, String anonKey) async {
db = PowerSyncDatabase(schema: schema, path: 'app_powersync.db');
await db.initialize();
// Supabaseコネクタ接続(ネットワークが接続されるとバックグラウンド同期を実行)
final connector = SupabaseConnector(url: supabaseUrl, anonKey: anonKey);
await db.connect(connector: connector);
}
ステップ3:Riverpod 3.0オフライン反応型UI購読(lib/providers/todo_provider.dart)
Riverpod 3.0ガイドで解説したAsyncNotifierとStream反応型パターンを組み合わせ、ネットワーク接続の有無にかかわらずUIが即座に反応するよう構成します。
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../database/app_database.dart';
final databaseProvider = Provider<AppDatabase>((ref) => AppDatabase());
// ローカルDrift DBのStreamをリアルタイム購読するRiverpod 3.0 StreamProvider
final todoListProvider = StreamProvider.autoDispose<List<Todo>>((ref) {
final db = ref.watch(databaseProvider);
return db.watchAllTodos();
});
// UIからTodo追加時:ネットワークリクエストなしでローカルDBに即座にInsert
class TodoNotifier extends AutoDisposeAsyncNotifier<void> {
@override
Future<void> build() async {}
Future<void> addTodo(String title) async {
final db = ref.read(databaseProvider);
await db.into(db.todos).insert(
TodosCompanion.insert(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
updatedAt: DateTime.now(),
),
);
}
}
4. ネットワーク切断および再接続の実戦テストパターン
オフラインファーストアプリの正当性を検証するには、**ネットワーク操作テスト(Network Manipulation Test)**を行う必要があります。
[テストシナリオ]
1. 機内モード(オフライン)有効化 ➔ アプリで新しいメモを5件作成 ➔ 0msで即座に画面反映を確認 ⭕️
2. アプリ終了後に再実行(オフライン状態) ➔ 作成したデータが100%ローカルで有効であることを確認 ⭕️
3. Wi-Fi再接続 ➔ PowerSyncがバックグラウンドでSupabase Postgresへ自動Sync ⭕️
4. Supabaseダッシュボード ➔ RLSポリシーの適用およびデータ衝突なしで送信完了を確認 ⭕️
この構造を適用することで、Flutter DevTools メモリリークおよびCPUプロファイリング実践ガイドで強調したUIスレッドのレンダリングラグが発生せず、フレームレート60〜120fpsが完璧に維持されます。
5. 2026年オフラインファーストライブラリ選定ガイド
| ライブラリ | オフラインファースト適合性 | 評価および推薦理由 |
|---|---|---|
| PowerSync + Drift | ★★★★★(推奨) | 2026年ベストプラクティス。partial sync、衝突解決、Driftの型安全性が完璧に調和 |
| Drift + Custom Outbox | ★★★★☆(カスタム) | サードパーティサービスなしで直接SyncキューとREST/GraphQLコネクタを作成する際に優れる |
| Supabase Realtime キャッシュ | ★★☆☆☆(非推奨) | オフライン作成不可。ネットワーク接続時のリアルタイム購読のみに特化 |
| Brick ORM | ★☆☆☆☆(非推奨) | サーバー側の削除非同期化の失敗および複雑なボイラープレートにより2026年時点で保守忌避 |
よくある質問
オフライン状態で複数のデバイスが同じデータを修正した場合、衝突は発生しませんか?
PowerSyncエンジンは**LWW(Last-Write-Wins)時系列ルールやCRDT(Conflict-free Replicated Data Type)**アルゴリズムを通じて、衝突をバックグラウンドで自動解決します。特殊なビジネスロジックが必要な場合は、Supabase Postgresトリガー関数やPowerSyncカスタム衝突ハンドラーを定義することで簡単に制御できます。
ローカルSQLite DBの容量が大きくなりすぎた場合はどうすればよいですか?
PowerSyncの**部分同期(Partial Sync Rules)**機能を活用すると、ユーザーが現在必要とするデータ(例:直近30日間のデータや自身が所有するデータ)のみを選択的にローカルデバイスにダウンロードできるため、デバイスのメモリとストレージ容量を最小化できます。
Supabaseのオフラインデータセキュリティは安全ですか?
ローカルDrift SQLiteデータベースファイルにsqlcipher暗号化エンジンを適用すると、デバイスの脱獄(Jailbreak)やルート化(Rooting)環境であってもデータベースファイルを復号化することはできません。また、Supabaseサーバー側のRLS(Row Level Security)ポリシーが再接続時に二次検証を行うため、セキュリティは厳格に維持されます。
Web(Flutter Web)プラットフォームでも同様にオフラインファーストが動作しますか?
動作します。Flutter Web環境では、DriftがWasm + IndexedDBバックエンドに自動切り替えされ、ブラウザ内部にオフラインSQLiteデータベースを生成します。Web環境でのWasmレンダリング性能については、Flutter Web Wasm + Skwasmガイドで詳しく解説しています。