🔄

Flutterでオフライン時のデータ競合解決とオンライン復帰時の安全なサーバー同期処理

に公開

ここから記事本文

はじめに

本記事はChatGPTによって生成されました。


1. 導入:テーマの概要や重要性

モバイルアプリ開発において、ネットワークの不安定さや接続切断はユーザー体験に大きな影響を与えます。特にFlutterのようなクロスプラットフォームフレームワークで開発されるアプリでは、オフライン時のデータ操作とオンライン復帰時のサーバー同期が重要な課題となります。ユーザーはオフライン中にデータを更新・追加したいが、ネットワーク再接続時にデータの競合が発生する恐れがあります。これを放置すると、データの不整合や上書きによる情報損失が起き、ユーザーの信頼を失いかねません。

本記事では、Flutterアプリにおいてオフライン時のデータ競合解決とオンライン復帰時のサーバーとの安全な同期処理を実現する方法を詳しく解説します。これにより、ローカルとリモート双方のデータの整合性を保ちつつ、快適かつ信頼性の高いユーザー体験を提供できるようになります。特に競合解決のアルゴリズムやデータ同期のフローを理解し、実装に活かすことで、堅牢なオフラインファースト設計が可能です。


2. 背景・基礎知識

オフラインファースト設計とは

オフラインファーストとは、アプリがネットワーク接続の有無に関わらず正常に動作する設計思想です。オフライン時はローカルデータベースやキャッシュを用いて更新を許可し、オンライン復帰時にサーバーへ同期します。

データ競合とは

複数のクライアントやオフライン状態でのデータ更新が同時に行われた場合に、どの更新を優先するか決められない状態を指します。競合を放置するとデータの破損や不整合が生じます。

競合解決の代表的手法

  • Last Write Wins (LWW)
    最後に更新されたデータを優先する。

  • マージロジック
    アプリケーション固有のロジックでデータを統合する。

  • オペレーショントランスフォーム (OT) / CRDT
    分散システムで競合を自動解決する高度なアルゴリズム。

Flutterでの一般的なオフライン同期手法

  • ローカルストレージ利用
    sqflitehiveなどでローカルにデータを保持。

  • 同期キューの管理
    オフライン時の変更をキューに溜め、オンライン復帰時に順次アップロード。

  • 状態管理
    RiverpodProviderで状態を管理し、UIと同期。


3. 本論:Flutterでの安全なデータ競合解決と同期処理

アーキテクチャ例

[ユーザー操作] → [ローカルDB更新] → [同期キュー追加]
                      ↓                ↑
                 [UI更新]           [オンライン検知]
                      ↓                ↓
                  [表示反映] ← [サーバー同期] ← [競合解決ロジック]

主要コンポーネント

  1. ローカルデータベース

    • 例: sqfliteで永続化
    • オフライン時の読み書きを担う
  2. 同期キュー管理

    • オフライン中の更新をキューに保存
    • ネットワーク復帰時に順次送信
  3. 競合解決ロジック

    • タイムスタンプでLWWを実装
    • 必要に応じてマージ処理
  4. ネットワーク状態検知

    • connectivity_plusでオンライン/オフライン判定
  5. サーバーAPI

    • 更新の受け入れとタイムスタンプ管理

フロー詳細

  1. データ更新時

    • ユーザーがデータを変更するとローカルDBへ書き込み
    • 同時に変更内容を同期キューに追加
  2. ネットワーク状態変化検知

    • オンライン復帰を検知したら同期キューから順次サーバーへ送信
  3. サーバーとの競合検出

    • サーバーは受信した更新のタイムスタンプを比較
    • 競合があれば解決ロジックを実行し、結果を返す
  4. ローカル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