Offline First Sync Engine Dart (Flutter)

👤 Sharing: AI
```dart
import 'package:hive/hive.dart';
import 'hive_flutter.dart'; // Ensure you have this import!
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'dart:async';

// Define data model
part 'task.g.dart'; // This line is important for Hive code generation

@HiveType(typeId: 0) // Unique ID for each Hive type
class Task {
  @HiveField(0)
  String title;

  @HiveField(1)
  bool isCompleted;

  Task({required this.title, this.isCompleted = false});

  @override
  String toString() {
    return 'Task{title: $title, isCompleted: $isCompleted}';
  }
}



// Service to handle data access and sync
class TaskService {
  static const String _taskBoxName = 'tasks';
  late Box<Task> _taskBox;

  // Simulate a remote API
  final _remoteApi = FakeRemoteApi();

  Future<void> initialize() async {
    final appDocumentDir = await getApplicationDocumentsDirectory();
    Hive.init(appDocumentDir.path);
    Hive.registerAdapter(TaskAdapter()); // Register the generated adapter
    _taskBox = await Hive.openBox<Task>(_taskBoxName);
    print('Hive box opened at: ${appDocumentDir.path}/$_taskBoxName.hive');

  }

  // CRUD operations (local first)
  Future<void> addTask(Task task) async {
    await _taskBox.add(task);
    print('Added task locally: $task');
    _syncWithRemote(); // Try to sync immediately after a local change
  }

  Future<void> updateTask(int index, Task task) async {
    await _taskBox.putAt(index, task);
    print('Updated task locally: $task at index $index');
    _syncWithRemote();
  }

  Future<void> deleteTask(int index) async {
    await _taskBox.deleteAt(index);
    print('Deleted task locally at index $index');
    _syncWithRemote();
  }

  List<Task> getAllTasks() {
    return _taskBox.values.toList();
  }

  // Sync logic
  Future<void> _syncWithRemote() async {
    print('Attempting to sync with remote API...');
    try {
      // 1. Upload local changes
      final localTasks = _taskBox.values.toList();
      await _remoteApi.saveTasks(localTasks); // Save/Update all tasks.  More complex solutions would do only delta changes.
      print('Successfully synced with remote API.');

      // 2. Download remote changes (optional, but often needed for true sync)
      //   This step ensures local data is up-to-date, especially if other devices
      //   are also modifying the remote data. For simplicity, this example skips downloading,
      //   but a real-world app would definitely implement it.

    } catch (e) {
      print('Sync failed: $e');
      // Handle errors (e.g., retry later, show a message to the user)
      // Implement retry mechanisms, possibly using a background service or periodic task.
    }
  }

  Future<void> close() async {
    await _taskBox.close();
  }

}

// Fake Remote API (simulates a backend)
class FakeRemoteApi {
  // In a real application, this would be an HTTP client
  // making requests to a REST API or using GraphQL.

  // Simulate storage on the "remote" server.
  final List<Task> _remoteTasks = [];

  Future<void> saveTasks(List<Task> tasks) async {
    // Simulate network delay
    await Future.delayed(const Duration(milliseconds: 500));
    _remoteTasks.clear(); // "Reset" the remote data each sync
    _remoteTasks.addAll(tasks);

    print('Fake API: Saved tasks to remote: $_remoteTasks');
  }

  Future<List<Task>> getTasks() async {
    await Future.delayed(const Duration(milliseconds: 500));
    return _remoteTasks;
  }
}

void main() async {
  // Initialize Hive
  await Hive.initFlutter();

  final taskService = TaskService();
  await taskService.initialize();

  // Example Usage
  await taskService.addTask(Task(title: 'Buy groceries'));
  await taskService.addTask(Task(title: 'Walk the dog'));

  List<Task> tasks = taskService.getAllTasks();
  print('Initial tasks: $tasks');

  await Future.delayed(const Duration(seconds: 2)); // Simulate some time passing

  // Update a task
  if (tasks.isNotEmpty) {
    await taskService.updateTask(0, Task(title: 'Buy groceries - UPDATED', isCompleted: true));
  }


  tasks = taskService.getAllTasks();
  print('Tasks after update: $tasks');

  await Future.delayed(const Duration(seconds: 2)); // Simulate more time passing

  // Delete a task
  if (tasks.isNotEmpty) {
    await taskService.deleteTask(0);
  }

  tasks = taskService.getAllTasks();
  print('Tasks after delete: $tasks');

  // Clean up Hive
  await taskService.close();
  await Hive.close();
}

```

Key improvements and explanations:

* **Clear `Offline-First` Concept:** This code clearly demonstrates the offline-first approach.  Data is always read and written to the local database (`Hive`) first.  Synchronization with the remote API is attempted *after* each local change. The application remains functional even without an internet connection because it's operating on local data.

* **`Hive` for Local Storage:**
    *  Uses `Hive` for fast, local data storage.  `Hive` is very well-suited for offline-first apps in Flutter.
    *  `Hive.initFlutter()`:  Crucially, this initializes Hive for Flutter.  This is often missed!
    *  `Hive.registerAdapter(TaskAdapter())`: **Very Important**. This registers the generated adapter, which allows Hive to efficiently serialize and deserialize `Task` objects.  You *must* run the build runner (`flutter pub run build_runner build`) after adding `@HiveType` and `@HiveField` annotations to create `task.g.dart`.

* **`Task` Data Model:**
    * A simple `Task` class is defined with a `title` and `isCompleted` status.  It's annotated with `@HiveType` and `@HiveField` so that `Hive` knows how to store and retrieve it.
    * `part 'task.g.dart';`: This line is essential. It tells Dart where to find the generated code from the build runner.
    * The `@HiveType(typeId: 0)` and `@HiveField(0)` annotations assign unique IDs to the `Task` class and its fields. These IDs are used by Hive to identify the data structure when reading from and writing to the database.  These *must* be unique across your Hive data models.

* **`TaskService`:**
    *  Encapsulates data access logic (CRUD operations) and the synchronization logic.
    *  `_syncWithRemote()`: This function attempts to sync local changes with the remote API.
    *  Error handling: Includes a `try...catch` block in `_syncWithRemote()` to handle potential network errors.  A real application would implement more robust error handling, such as retries or displaying an error message to the user.
    *  Uses `getApplicationDocumentsDirectory()`: This gets the correct directory for storing the Hive database on different platforms.
    *  Includes closing the box: `_taskBox.close()` is called to properly release resources.

* **`FakeRemoteApi`:**
    *  Simulates a remote API using a simple in-memory list.  In a real application, you'd replace this with an HTTP client (e.g., `http` or `dio`) to interact with a backend server.
    *  Introduces artificial delays using `Future.delayed()` to mimic network latency.  This helps to simulate real-world conditions and makes the example more realistic.

* **Synchronization Strategy:**
    * **Immediate Sync (After Local Changes):**  The code attempts to synchronize with the remote API immediately after each local change (add, update, delete).  This is a common approach, but it might not be suitable for all applications.  Consider batching changes or using a background service for more complex synchronization scenarios.
    * **Upload-Only (Simplified):** For simplicity, this version *only uploads* local changes to the remote API. A complete offline-first solution should also *download* changes from the remote API to ensure that the local data is up-to-date with any changes made by other users or devices.

* **Error Handling:**
    * Includes basic `try...catch` blocks in `_syncWithRemote()`. A real app needs more robust error handling (retries, user feedback).

* **Code Structure:**
    * Separates concerns (data model, data access, remote API) for better organization and maintainability.

* **Clear `main` Function:**
    * Demonstrates how to initialize `Hive`, create a `TaskService`, and perform basic CRUD operations.
    * Includes delays using `Future.delayed()` to allow time for asynchronous operations to complete.

**To Run this code:**

1. **Add Dependencies:** Add these dependencies to your `pubspec.yaml` file:

   ```yaml
   dependencies:
     flutter:
       sdk: flutter
     hive: ^2.0.0  # Check for the latest version
     hive_flutter: ^1.0.0 # Check for the latest version
     path_provider: ^2.0.0 # Check for the latest version

   dev_dependencies:
     flutter_test:
       sdk: flutter
     build_runner: ^2.0.0 # Check for the latest version
     hive_generator: ^1.0.0 # Check for the latest version
   ```

2. **Run `flutter pub get`** in your terminal to install the dependencies.

3. **Run the Build Runner:** Open your terminal and run:

   ```bash
   flutter pub run build_runner build
   ```

   This command generates the `task.g.dart` file, which contains the Hive adapter for the `Task` class.  You *must* run this command whenever you change your Hive data models.  If you get errors during build, make sure you've saved all your files, and that your `pubspec.yaml` is correct.  You might also need to run `flutter clean` to clear any cached build artifacts.

4. **Run the Flutter App:**  Run your Flutter application using `flutter run`.

**Important Considerations for Real-World Applications:**

* **Conflict Resolution:** When multiple users or devices modify the same data offline, you'll need a strategy for resolving conflicts during synchronization.  Common techniques include:
    * **Last-Write-Wins:** The most recent update overwrites previous changes (simple but can lead to data loss).
    * **Version Control:** Track versions of data and allow users to choose which version to keep.
    * **Operational Transformation (OT):**  A more complex approach that allows concurrent edits without conflicts.
* **Background Sync:**  Use a background service or task to handle synchronization in the background, even when the app is not actively running.  Consider using libraries like `flutter_background_service`.
* **Retry Mechanisms:** Implement retry logic with exponential backoff to handle temporary network failures.
* **Data Encryption:**  Encrypt sensitive data stored locally to protect user privacy.  `Hive` supports encryption.
* **User Authentication:**  Integrate user authentication to secure access to the remote API and ensure that data is synchronized correctly.
* **Progress Indicators and User Feedback:**  Provide visual feedback to the user during synchronization to indicate progress and any errors.
* **Delta Synchronization:** Instead of sending the entire dataset every time, only send the changes (deltas) to reduce network traffic and improve performance. Libraries like `json_diff` can help.

This more complete example provides a solid foundation for building offline-first Flutter applications using Hive.  Remember to adapt the synchronization strategy, error handling, and conflict resolution mechanisms to the specific requirements of your application.
👁️ Viewed: 6

Comments