🔄

オフライン時のデータ同期戦略:HiveとバックエンドAPI連携のベストプラクティス

に公開

ここから記事本文

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

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

現代のモバイルアプリケーションや分散システムにおいて、ネットワーク接続が不安定な状況下でのデータ同期は非常に重要な課題です。特に、ユーザーがオフラインの状態でデータを操作した後、再びオンラインになった際に、ローカルデータベースとバックエンドのサーバー間で整合性を保ちながらデータを同期する仕組みは、ユーザー体験の品質向上やシステムの信頼性向上に直結します。

Hiveは主に大規模分散データの分析処理に用いられるSQLライクなデータウェアハウスソフトウェアですが、モバイルアプリやクライアント側のローカルストレージとして利用されるHive(Dart/FlutterのNoSQL型ローカルDB)と混同しやすいため、本記事では後者のHive(Flutterで利用される軽量ローカルDB)を対象とし、バックエンドAPIとの効果的なデータ同期戦略に焦点を当てます。

オフライン時のデータ処理は、単にローカルに保存するだけでなく、APIを通じたバックエンドとの同期のタイミング制御、競合解決、データ整合性の担保など複雑な問題をはらんでいます。これらを適切に設計・実装することは、UXの向上や運用コスト削減に不可欠です。本記事では、HiveとバックエンドAPIの連携におけるベストプラクティスを体系的に解説します。


2. 背景・基礎知識

Hive(Flutter)の概要

Hiveは、Dart言語向けに開発された高速で軽量なNoSQL型ローカルデータベースで、Flutterアプリのローカル保存に広く使われています。IndexedDBやSQLiteの代替として、スキーマレス、バイナリ形式で高速に読み書きできる点が特徴です。

Hiveの概念図

  • Box:Hiveにおけるデータコンテナ。Key-Value形式でデータを格納。
  • Adapter:カスタムオブジェクトをHiveに格納するためのシリアライズ手法。

オフライン同期の基本概念

  • ローカルキャッシュ:オフライン時に操作したデータを一時的に保存。
  • キューイング:オフライン操作のAPIリクエストをキューに保存。
  • 再同期(リコンサイル):オンライン復帰時にサーバーと差分同期。
  • 競合解決:同じデータがローカルとサーバーで異なる場合の調整。

バックエンドAPIの役割

RESTfulまたはGraphQL APIを通じて、クライアントのローカルデータとサーバーデータの同期を実現。APIはデータ取得・更新・削除操作を提供し、同期ロジックはクライアント側で制御します。


3. 本論:技術的な詳細や仕組み、手順

HiveとバックエンドAPIを連携したオフライン同期の典型的なアーキテクチャは以下の通りです。

[ユーザー操作] → [Hive(ローカルDB)] → [同期キュー] → [バックエンドAPI]
                                          ↑
                                     オンライン検知

主な処理フロー

  1. 読み込み

    • アプリ起動時にHiveからローカルデータ読み込み。
    • バックエンドから最終更新時刻以降の差分データを取得してHiveに反映。
  2. 書き込み(オフライン含む)

    • ユーザー操作によるデータ変更をHiveに即時反映。
    • 変更内容を同期キューに追加(ローカルのListや別Boxに保存)。
  3. 同期処理

    • ネットワーク復帰検知時に同期キューを順次APIに送信。
    • API側で処理成功後、キューから該当データを削除。
    • APIレスポンスの内容によりHiveのローカルデータを更新。
  4. 競合検知と解決

    • タイムスタンプやバージョン管理を用いて競合を検知。
    • 解決ルール(最新更新優先、ユーザー選択など)を適用。

同期用API設計例

  • GET /items?updated_since={timestamp}:差分取得
  • POST /items:新規作成
  • PUT /items/{id}:更新
  • DELETE /items/{id}:削除

4. 具体例・コード例

以下はFlutterアプリでHiveを用いて基本的なオフライン同期を実装するサンプルコードです。

import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;

part 'item.g.dart';

// Hive用のデータモデル
(typeId: 0)
class Item extends HiveObject {
  (0)
  String id;

  (1)
  String content;

  (2)
  DateTime updatedAt;

  Item({required this.id, required this.content, required this.updatedAt});
}

class SyncService {
  final Box<Item> itemBox;
  final Box<Map> syncQueueBox;

  SyncService(this.itemBox, this.syncQueueBox);

  Future<void> enqueueChange(Item item, String action) async {
    // action: create, update, delete
    final change = {
      'id': item.id,
      'content': item.content,
      'updatedAt': item.updatedAt.toIso8601String(),
      'action': action,
    };
    await syncQueueBox.add(change);
  }

  Future<void> syncWithServer() async {
    if (syncQueueBox.isEmpty) return;

    for (int i = 0; i < syncQueueBox.length; i++) {
      final change = syncQueueBox.getAt(i);
      if (change == null) continue;

      final response = await _sendChangeToApi(change);
      if (response) {
        await syncQueueBox.deleteAt(i);
        i--;
      }
    }
  }

  Future<bool> _sendChangeToApi(Map change) async {
    final String action = change['action'];
    final String id = change['id'];
    final String content = change['content'];
    final String updatedAt = change['updatedAt'];

    final uri = Uri.parse('https://api.example.com/items${action == 'create' ? '' : '/$id'}');
    http.Response response;

    try {
      if (action == 'create') {
        response = await http.post(uri, body: jsonEncode({'content': content, 'updatedAt': updatedAt}), headers: {'Content-Type': 'application/json'});
      } else if (action == 'update') {
        response = await http.put(uri, body: jsonEncode({'content': content, 'updatedAt': updatedAt}), headers: {'Content-Type': 'application/json'});
      } else if (action == 'delete') {
        response = await http.delete(uri);
      } else {
        return false;
      }

      return response.statusCode == 200 || response.statusCode == 201;
    } catch (e) {
      return false;
    }
  }
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final dir = await getApplicationDocumentsDirectory();
  Hive.init(dir.path);
  Hive.registerAdapter(ItemAdapter());

  final itemBox = await Hive.openBox<Item>('items');
  final syncQueueBox = await Hive.openBox<Map>('syncQueue');

  final syncService = SyncService(itemBox, syncQueueBox);

  runApp(MyApp(syncService: syncService, itemBox: itemBox));
}

class MyApp extends StatelessWidget {
  final SyncService syncService;
  final Box<Item> itemBox;

  MyApp({required this.syncService, required this.itemBox});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Hive オフライン同期例')),
        body: Center(
          child: ElevatedButton(
            onPressed: ()

Discussion