offline_first_sync 1.1.5 copy "offline_first_sync: ^1.1.5" to clipboard
offline_first_sync: ^1.1.5 copied to clipboard

Intelligent offline-first data management with automatic sync, conflict resolution, and visual status indicators

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:offline_first_sync/offline_first_sync.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Offline First Sync Demo',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const SyncDemoPage(),
    );
  }
}

class SyncDemoPage extends StatefulWidget {
  const SyncDemoPage({super.key});

  @override
  State<SyncDemoPage> createState() => _SyncDemoPageState();
}

class _SyncDemoPageState extends State<SyncDemoPage> {
  late SyncManager _syncManager;
  final List<TodoItem> _todos = [];
  final TextEditingController _textController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _initializeSyncManager();
  }

  void _initializeSyncManager() {
    final client = HttpSyncClient(
      baseUrl: 'https://api.example.com',
      headers: {'Authorization': 'Bearer your-token'},
    );

    _syncManager = SyncManager(
      client: client,
      syncInterval: const Duration(seconds: 30),
    );

    // Listen to conflicts for resolution
    _syncManager.conflictsStream.listen((conflicts) {
      if (conflicts.isNotEmpty) {
        _showConflictDialog(conflicts.first);
      }
    });
  }

  void _addTodo() async {
    if (_textController.text.trim().isEmpty) return;

    final todo = TodoItem(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      title: _textController.text.trim(),
      completed: false,
      createdAt: DateTime.now(),
    );

    setState(() {
      _todos.add(todo);
    });

    await _syncManager.addSyncItem(
      entityType: 'todos',
      entityId: todo.id,
      data: todo.toMap(),
      action: SyncAction.create,
    );

    _textController.clear();
  }

  void _toggleTodo(TodoItem todo) async {
    final updatedTodo = todo.copyWith(completed: !todo.completed);

    setState(() {
      final index = _todos.indexWhere((t) => t.id == todo.id);
      if (index != -1) {
        _todos[index] = updatedTodo;
      }
    });

    await _syncManager.addSyncItem(
      entityType: 'todos',
      entityId: todo.id,
      data: updatedTodo.toMap(),
      action: SyncAction.update,
    );
  }

  void _deleteTodo(TodoItem todo) async {
    setState(() {
      _todos.removeWhere((t) => t.id == todo.id);
    });

    await _syncManager.addSyncItem(
      entityType: 'todos',
      entityId: todo.id,
      data: todo.toMap(),
      action: SyncAction.delete,
    );
  }

  void _showConflictDialog(SyncItem conflictItem) {
    showDialog(
      context: context,
      builder: (context) => ConflictResolutionDialog(
        conflictItem: conflictItem,
        onResolve: (strategy, data) async {
          await _syncManager.resolveConflict(
            conflictItem.id,
            strategy,
            customData: data,
          );
          Navigator.of(context).pop();
        },
      ),
    );
  }

  void _showSyncDetails() {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      builder: (context) => SyncDetailsSheet(syncManager: _syncManager),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Offline First Sync Demo'),
        actions: [
          StreamBuilder<SyncStatus>(
            stream: _syncManager.statusStream,
            builder: (context, snapshot) {
              final status =
                  snapshot.data ??
                  const SyncStatus(
                    isOnline: false,
                    isSyncing: false,
                    pendingCount: 0,
                    failedCount: 0,
                    conflictCount: 0,
                  );
              return Padding(
                padding: const EdgeInsets.only(right: 16),
                child: SyncStatusIndicator(
                  status: status,
                  onTap: _showSyncDetails,
                ),
              );
            },
          ),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _textController,
                    decoration: const InputDecoration(
                      hintText: 'Add a new todo...',
                      border: OutlineInputBorder(),
                    ),
                    onSubmitted: (_) => _addTodo(),
                  ),
                ),
                const SizedBox(width: 16),
                ElevatedButton(onPressed: _addTodo, child: const Text('Add')),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _todos.length,
              itemBuilder: (context, index) {
                final todo = _todos[index];
                return ListTile(
                  leading: Checkbox(
                    value: todo.completed,
                    onChanged: (_) => _toggleTodo(todo),
                  ),
                  title: Text(
                    todo.title,
                    style: TextStyle(
                      decoration: todo.completed
                          ? TextDecoration.lineThrough
                          : null,
                    ),
                  ),
                  trailing: IconButton(
                    icon: const Icon(Icons.delete),
                    onPressed: () => _deleteTodo(todo),
                  ),
                );
              },
            ),
          ),
        ],
      ),
      floatingActionButton: StreamBuilder<SyncStatus>(
        stream: _syncManager.statusStream,
        builder: (context, snapshot) {
          final status =
              snapshot.data ??
              const SyncStatus(
                isOnline: false,
                isSyncing: false,
                pendingCount: 0,
                failedCount: 0,
                conflictCount: 0,
              );

          return SyncFab(status: status, onPressed: () => _syncManager.sync());
        },
      ),
    );
  }

  @override
  void dispose() {
    _syncManager.dispose();
    _textController.dispose();
    super.dispose();
  }
}

class TodoItem {
  final String id;
  final String title;
  final bool completed;
  final DateTime createdAt;

  const TodoItem({
    required this.id,
    required this.title,
    required this.completed,
    required this.createdAt,
  });

  TodoItem copyWith({
    String? id,
    String? title,
    bool? completed,
    DateTime? createdAt,
  }) {
    return TodoItem(
      id: id ?? this.id,
      title: title ?? this.title,
      completed: completed ?? this.completed,
      createdAt: createdAt ?? this.createdAt,
    );
  }

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'title': title,
      'completed': completed,
      'createdAt': createdAt.toIso8601String(),
    };
  }

  factory TodoItem.fromMap(Map<String, dynamic> map) {
    return TodoItem(
      id: map['id'],
      title: map['title'],
      completed: map['completed'],
      createdAt: DateTime.parse(map['createdAt']),
    );
  }
}

class ConflictResolutionDialog extends StatefulWidget {
  final SyncItem conflictItem;
  final Function(ConflictResolutionStrategy, Map<String, dynamic>?) onResolve;

  const ConflictResolutionDialog({
    super.key,
    required this.conflictItem,
    required this.onResolve,
  });

  @override
  State<ConflictResolutionDialog> createState() =>
      _ConflictResolutionDialogState();
}

class _ConflictResolutionDialogState extends State<ConflictResolutionDialog> {
  ConflictResolutionStrategy _selectedStrategy =
      ConflictResolutionStrategy.clientWins;

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('Sync Conflict Detected'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'A conflict occurred while syncing ${widget.conflictItem.entityType}.',
          ),
          const SizedBox(height: 16),
          const Text(
            'Client Data:',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          Container(
            padding: const EdgeInsets.all(8),
            margin: const EdgeInsets.symmetric(vertical: 4),
            decoration: BoxDecoration(
              color: Colors.blue.shade50,
              borderRadius: BorderRadius.circular(4),
            ),
            child: Text(widget.conflictItem.data.toString()),
          ),
          const Text(
            'Server Data:',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          Container(
            padding: const EdgeInsets.all(8),
            margin: const EdgeInsets.symmetric(vertical: 4),
            decoration: BoxDecoration(
              color: Colors.orange.shade50,
              borderRadius: BorderRadius.circular(4),
            ),
            child: Text(
              widget.conflictItem.serverData?.toString() ?? 'No server data',
            ),
          ),
          const SizedBox(height: 16),
          const Text('Resolution Strategy:'),
          ...ConflictResolutionStrategy.values.map((strategy) {
            return RadioListTile<ConflictResolutionStrategy>(
              title: Text(_getStrategyDisplayName(strategy)),
              subtitle: Text(_getStrategyDescription(strategy)),
              value: strategy,
              groupValue: _selectedStrategy,
              onChanged: (value) {
                setState(() {
                  _selectedStrategy = value!;
                });
              },
            );
          }),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('Cancel'),
        ),
        ElevatedButton(
          onPressed: () => widget.onResolve(_selectedStrategy, null),
          child: const Text('Resolve'),
        ),
      ],
    );
  }

  String _getStrategyDisplayName(ConflictResolutionStrategy strategy) {
    switch (strategy) {
      case ConflictResolutionStrategy.clientWins:
        return 'Keep Local Changes';
      case ConflictResolutionStrategy.serverWins:
        return 'Use Server Version';
      case ConflictResolutionStrategy.lastWriteWins:
        return 'Last Write Wins';
      case ConflictResolutionStrategy.merge:
        return 'Merge Changes';
      case ConflictResolutionStrategy.manual:
        return 'Manual Resolution';
    }
  }

  String _getStrategyDescription(ConflictResolutionStrategy strategy) {
    switch (strategy) {
      case ConflictResolutionStrategy.clientWins:
        return 'Overwrite server with local changes';
      case ConflictResolutionStrategy.serverWins:
        return 'Discard local changes and use server version';
      case ConflictResolutionStrategy.lastWriteWins:
        return 'Use the most recently modified version';
      case ConflictResolutionStrategy.merge:
        return 'Automatically merge both versions';
      case ConflictResolutionStrategy.manual:
        return 'Manually edit the data';
    }
  }
}

class SyncDetailsSheet extends StatelessWidget {
  final SyncManager syncManager;

  const SyncDetailsSheet({super.key, required this.syncManager});

  @override
  Widget build(BuildContext context) {
    return DraggableScrollableSheet(
      initialChildSize: 0.6,
      maxChildSize: 0.9,
      minChildSize: 0.3,
      builder: (context, scrollController) {
        return Container(
          decoration: const BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
          ),
          child: Column(
            children: [
              Container(
                width: 40,
                height: 4,
                margin: const EdgeInsets.symmetric(vertical: 8),
                decoration: BoxDecoration(
                  color: Colors.grey.shade300,
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(16),
                child: Row(
                  children: [
                    const Text(
                      'Sync Details',
                      style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const Spacer(),
                    ElevatedButton(
                      onPressed: () => syncManager.sync(),
                      child: const Text('Sync Now'),
                    ),
                  ],
                ),
              ),
              Expanded(
                child: StreamBuilder<SyncStatus>(
                  stream: syncManager.statusStream,
                  builder: (context, snapshot) {
                    final status =
                        snapshot.data ??
                        const SyncStatus(
                          isOnline: false,
                          isSyncing: false,
                          pendingCount: 0,
                          failedCount: 0,
                          conflictCount: 0,
                        );

                    return ListView(
                      controller: scrollController,
                      padding: const EdgeInsets.all(16),
                      children: [
                        _buildStatusCard(
                          'Connection',
                          status.isOnline ? 'Online' : 'Offline',
                          status.isOnline ? Colors.green : Colors.red,
                        ),
                        _buildStatusCard(
                          'Sync Status',
                          status.isSyncing ? 'Syncing...' : 'Idle',
                          status.isSyncing ? Colors.blue : Colors.grey,
                        ),
                        _buildStatusCard(
                          'Pending Items',
                          '${status.pendingCount}',
                          status.pendingCount > 0
                              ? Colors.orange
                              : Colors.green,
                        ),
                        _buildStatusCard(
                          'Failed Items',
                          '${status.failedCount}',
                          status.failedCount > 0 ? Colors.red : Colors.green,
                        ),
                        _buildStatusCard(
                          'Conflicts',
                          '${status.conflictCount}',
                          status.conflictCount > 0 ? Colors.red : Colors.green,
                        ),
                        if (status.lastSyncTime != null)
                          _buildStatusCard(
                            'Last Sync',
                            _formatDateTime(status.lastSyncTime!),
                            Colors.grey,
                          ),
                        const SizedBox(height: 16),
                        if (status.failedCount > 0)
                          ElevatedButton.icon(
                            onPressed: () => syncManager.retryFailed(),
                            icon: const Icon(Icons.refresh),
                            label: const Text('Retry Failed Items'),
                          ),
                        if (status.pendingCount > 0 || status.failedCount > 0)
                          TextButton.icon(
                            onPressed: () => _showClearConfirmation(context),
                            icon: const Icon(Icons.clear_all),
                            label: const Text('Clear Sync Queue'),
                            style: TextButton.styleFrom(
                              foregroundColor: Colors.red,
                            ),
                          ),
                      ],
                    );
                  },
                ),
              ),
            ],
          ),
        );
      },
    );
  }

  Widget _buildStatusCard(String title, String value, Color color) {
    return Card(
      child: ListTile(
        title: Text(title),
        trailing: Container(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
          decoration: BoxDecoration(
            color: color.withOpacity(0.1),
            borderRadius: BorderRadius.circular(12),
            border: Border.all(color: color.withOpacity(0.3)),
          ),
          child: Text(
            value,
            style: TextStyle(color: color, fontWeight: FontWeight.w500),
          ),
        ),
      ),
    );
  }

  String _formatDateTime(DateTime dateTime) {
    final now = DateTime.now();
    final difference = now.difference(dateTime);

    if (difference.inMinutes < 1) {
      return 'Just now';
    } else if (difference.inHours < 1) {
      return '${difference.inMinutes}m ago';
    } else if (difference.inDays < 1) {
      return '${difference.inHours}h ago';
    } else {
      return '${difference.inDays}d ago';
    }
  }

  void _showClearConfirmation(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Clear Sync Queue'),
        content: const Text(
          'This will remove all pending sync items. This action cannot be undone.',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('Cancel'),
          ),
          ElevatedButton(
            onPressed: () {
              syncManager.clearSyncQueue();
              Navigator.of(context).pop();
              Navigator.of(context).pop();
            },
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.red,
              foregroundColor: Colors.white,
            ),
            child: const Text('Clear'),
          ),
        ],
      ),
    );
  }
}
1
likes
140
points
6
downloads

Publisher

verified publishersanjaysharma.info

Weekly Downloads

Intelligent offline-first data management with automatic sync, conflict resolution, and visual status indicators

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

connectivity_plus, flutter, http, path, rxdart, sqflite, uuid

More

Packages that depend on offline_first_sync