Flutterでオフライン時のデータ競合解決とオンライン復帰時の安全なサーバー同期処理
ここから記事本文
はじめに
本記事はChatGPTによって生成されました。
1. 導入:テーマの概要や重要性
モバイルアプリ開発において、ネットワークの不安定さや接続切断はユーザー体験に大きな影響を与えます。特にFlutterのようなクロスプラットフォームフレームワークで開発されるアプリでは、オフライン時のデータ操作とオンライン復帰時のサーバー同期が重要な課題となります。ユーザーはオフライン中にデータを更新・追加したいが、ネットワーク再接続時にデータの競合が発生する恐れがあります。これを放置すると、データの不整合や上書きによる情報損失が起き、ユーザーの信頼を失いかねません。
本記事では、Flutterアプリにおいてオフライン時のデータ競合解決とオンライン復帰時のサーバーとの安全な同期処理を実現する方法を詳しく解説します。これにより、ローカルとリモート双方のデータの整合性を保ちつつ、快適かつ信頼性の高いユーザー体験を提供できるようになります。特に競合解決のアルゴリズムやデータ同期のフローを理解し、実装に活かすことで、堅牢なオフラインファースト設計が可能です。
2. 背景・基礎知識
オフラインファースト設計とは
オフラインファーストとは、アプリがネットワーク接続の有無に関わらず正常に動作する設計思想です。オフライン時はローカルデータベースやキャッシュを用いて更新を許可し、オンライン復帰時にサーバーへ同期します。
データ競合とは
複数のクライアントやオフライン状態でのデータ更新が同時に行われた場合に、どの更新を優先するか決められない状態を指します。競合を放置するとデータの破損や不整合が生じます。
競合解決の代表的手法
-
Last Write Wins (LWW)
最後に更新されたデータを優先する。 -
マージロジック
アプリケーション固有のロジックでデータを統合する。 -
オペレーショントランスフォーム (OT) / CRDT
分散システムで競合を自動解決する高度なアルゴリズム。
Flutterでの一般的なオフライン同期手法
-
ローカルストレージ利用
sqflite
やhive
などでローカルにデータを保持。 -
同期キューの管理
オフライン時の変更をキューに溜め、オンライン復帰時に順次アップロード。 -
状態管理
Riverpod
やProvider
で状態を管理し、UIと同期。
3. 本論:Flutterでの安全なデータ競合解決と同期処理
アーキテクチャ例
[ユーザー操作] → [ローカルDB更新] → [同期キュー追加]
↓ ↑
[UI更新] [オンライン検知]
↓ ↓
[表示反映] ← [サーバー同期] ← [競合解決ロジック]
主要コンポーネント
-
ローカルデータベース
- 例:
sqflite
で永続化 - オフライン時の読み書きを担う
- 例:
-
同期キュー管理
- オフライン中の更新をキューに保存
- ネットワーク復帰時に順次送信
-
競合解決ロジック
- タイムスタンプでLWWを実装
- 必要に応じてマージ処理
-
ネットワーク状態検知
-
connectivity_plus
でオンライン/オフライン判定
-
-
サーバーAPI
- 更新の受け入れとタイムスタンプ管理
フロー詳細
-
データ更新時
- ユーザーがデータを変更するとローカルDBへ書き込み
- 同時に変更内容を同期キューに追加
-
ネットワーク状態変化検知
- オンライン復帰を検知したら同期キューから順次サーバーへ送信
-
サーバーとの競合検出
- サーバーは受信した更新のタイムスタンプを比較
- 競合があれば解決ロジックを実行し、結果を返す
-
ローカルDBとサーバーの整合性更新
- サーバーの最新データをローカルに反映し、UIも更新
4. 具体例・コード例
以下は簡単な実装例です。
- ローカルDBは
hive
- ネットワーク監視は
connectivity_plus
- 競合解決はLWW方式でタイムスタンプを比較
依存関係(pubspec.yaml)
dependencies:
flutter:
sdk: flutter
hive: ^2.2.3
hive_flutter: ^1.1.0
connectivity_plus: ^4.0.0
http: ^0.13.5
データモデル例
import 'package:hive/hive.dart';
part 'note.g.dart';
(typeId: 0)
class Note extends HiveObject {
(0)
String id;
(1)
String content;
(2)
DateTime updatedAt;
Note({
required this.id,
required this.content,
required this.updatedAt,
});
}
同期キュー管理クラス
class SyncQueue {
final Box<Note> _localBox;
final List<Note> _pendingUpdates = [];
SyncQueue(this._localBox);
void addUpdate(Note note) {
_pendingUpdates.add(note);
_localBox.put(note.id, note);
}
Future<void> syncToServer() async {
for (var note in _pendingUpdates) {
final success = await _uploadNoteToServer(note);
if (success) {
_pendingUpdates.remove(note);
}
}
}
Future<bool> _uploadNoteToServer(Note note) async {
// サーバーへHTTPリクエスト送信(例)
// 競合解決はサーバー側に任せて結果を取得
final response = await http.post(
Uri.parse('https://example.com/api/notes/${note.id}'),
body: {
'content': note.content,
'updatedAt': note.updatedAt.toIso8601String(),
},
);
if (response.statusCode == 200) {
// サーバーの応答を処理し、ローカルDBを更新
// 競合解決済みの最新データを受け取る想定
final serverData = parseNoteFromResponse(response.body);
_localBox.put(serverData.id, serverData);
return true;
}
return false;
}
}
ネットワーク検知と同期開始
import 'package:connectivity_plus/connectivity_plus.dart';
class NetworkSyncManager {
final SyncQueue syncQueue;
NetworkSyncManager(this.syncQueue) {
Connectivity().onConnectivityChanged.listen((status) {
if (status != ConnectivityResult.none) {
syncQueue.syncToServer();
}
});
}
}
UIからの利用例
void updateNoteContent(String id, String newContent) {
final now = DateTime.now();
final note = Note(id: id, content: newContent, updatedAt: now);
syncQueue.addUpdate(note);
}
5. 応用・発展
-
CRDTの導入
複雑な競合解決にはCRDT(Conflict-free Replicated Data Types)を用いることで、分散環境でも自動的に競合を解消可能。 -
リアルタイム同期
WebSocketやFirebase Realtime Databaseを用いてリアルタイムに変更を反映。 -
**バックグラウ
Discussion