オフライン時のデータ同期戦略: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の代替として、スキーマレス、バイナリ形式で高速に読み書きできる点が特徴です。
- Box:Hiveにおけるデータコンテナ。Key-Value形式でデータを格納。
- Adapter:カスタムオブジェクトをHiveに格納するためのシリアライズ手法。
オフライン同期の基本概念
- ローカルキャッシュ:オフライン時に操作したデータを一時的に保存。
- キューイング:オフライン操作のAPIリクエストをキューに保存。
- 再同期(リコンサイル):オンライン復帰時にサーバーと差分同期。
- 競合解決:同じデータがローカルとサーバーで異なる場合の調整。
バックエンドAPIの役割
RESTfulまたはGraphQL APIを通じて、クライアントのローカルデータとサーバーデータの同期を実現。APIはデータ取得・更新・削除操作を提供し、同期ロジックはクライアント側で制御します。
3. 本論:技術的な詳細や仕組み、手順
HiveとバックエンドAPIを連携したオフライン同期の典型的なアーキテクチャは以下の通りです。
[ユーザー操作] → [Hive(ローカルDB)] → [同期キュー] → [バックエンドAPI]
↑
オンライン検知
主な処理フロー
-
読み込み
- アプリ起動時にHiveからローカルデータ読み込み。
- バックエンドから最終更新時刻以降の差分データを取得してHiveに反映。
-
書き込み(オフライン含む)
- ユーザー操作によるデータ変更をHiveに即時反映。
- 変更内容を同期キューに追加(ローカルのListや別Boxに保存)。
-
同期処理
- ネットワーク復帰検知時に同期キューを順次APIに送信。
- API側で処理成功後、キューから該当データを削除。
- APIレスポンスの内容によりHiveのローカルデータを更新。
-
競合検知と解決
- タイムスタンプやバージョン管理を用いて競合を検知。
- 解決ルール(最新更新優先、ユーザー選択など)を適用。
同期用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