syncly_flutter 0.1.0
syncly_flutter: ^0.1.0 copied to clipboard
Backend-agnostic offline synchronization with a durable SQLite outbox, retries, pulls, tombstones, and conflict handling.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:syncly_flutter/syncly_flutter.dart';
import 'demo_sync_transport.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final localStorage = LocalStorage();
await localStorage.init();
final syncManager = SyncManager(
localStorage: localStorage,
transport: DemoSyncTransport(),
connectivityMonitor: ConnectivityMonitor(),
conflictHandler: ConflictHandler(),
);
runApp(MyApp(syncManager: syncManager));
}
class MyApp extends StatelessWidget {
final SyncManager syncManager;
const MyApp({super.key, required this.syncManager});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Offline Task Sync',
theme: ThemeData(primarySwatch: Colors.blue),
home: HomeScreen(syncManager: syncManager),
);
}
}
class HomeScreen extends StatefulWidget {
final SyncManager syncManager;
const HomeScreen({super.key, required this.syncManager});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
enum TaskFilter { all, synced, unsynced }
class _HomeScreenState extends State<HomeScreen> {
final TextEditingController _taskController = TextEditingController();
List<SyncData> _tasks = [];
String _syncStatus = 'Idle';
TaskFilter _currentFilter = TaskFilter.all;
bool _isSyncing = false;
late StreamSubscription<SyncEvent> _syncSubscription;
@override
void initState() {
super.initState();
unawaited(_loadTasks());
_syncSubscription = widget.syncManager.onSyncEvent.listen((event) {
if (mounted) {
setState(() {
_syncStatus = _formatSyncStatus(event);
});
}
if (event.type == SyncEventType.dataSynced ||
event.type == SyncEventType.dataUnsynced ||
event.type == SyncEventType.syncCompleted) {
unawaited(_loadTasks());
}
});
}
String _formatSyncStatus(SyncEvent event) {
switch (event.type) {
case SyncEventType.syncStarted:
_isSyncing = true;
return 'Syncing...';
case SyncEventType.syncCompleted:
_isSyncing = false;
return 'All data synced!';
case SyncEventType.syncFailed:
_isSyncing = false;
return 'Sync failed. Retrying...';
case SyncEventType.dataSynced:
if (event.data is SyncMutation) {
return 'Backend acknowledged ${(event.data as SyncMutation).id}';
}
return 'Data synced successfully.';
case SyncEventType.dataUnsynced:
return 'Task saved offline.';
case SyncEventType.connectionRestored:
return 'Connection restored. Syncing now...';
case SyncEventType.connectionLost:
return 'Offline mode. Data will sync when connected.';
case SyncEventType.dataDeleted:
return 'Task deletion queued.';
case SyncEventType.dataPulled:
return 'Remote changes applied.';
case SyncEventType.retryScheduled:
return 'Sync failed. A retry is scheduled.';
default:
_isSyncing = false;
return 'Idle';
}
}
Future<void> _loadTasks() async {
List<SyncData> tasks;
switch (_currentFilter) {
case TaskFilter.synced:
tasks = await widget.syncManager.localStorage.getSyncedData();
break;
case TaskFilter.unsynced:
tasks = await widget.syncManager.localStorage.getUnsyncedData();
break;
case TaskFilter.all:
tasks = await widget.syncManager.localStorage.getAllData();
break;
}
if (mounted) {
setState(() {
_tasks = tasks;
});
}
}
Future<void> _addTask() async {
final taskText = _taskController.text.trim();
if (taskText.isEmpty) return;
final newTask = {
'id': DateTime.now().millisecondsSinceEpoch.toString(),
'title': taskText,
'completed': false,
};
await widget.syncManager.addOfflineData('tasks', newTask);
_taskController.clear();
await _loadTasks();
}
Future<void> _toggleTask(SyncData task) async {
final updatedTask = task.copyWith(
data: {...task.data, 'completed': !(task.data['completed'] as bool)},
updatedAt: DateTime.now(),
);
await widget.syncManager.save(task.key, updatedTask.data);
await _loadTasks();
}
Future<void> _deleteTask(SyncData task) async {
await widget.syncManager.delete(task.id);
await _loadTasks();
}
Future<void> _manualSync() async {
setState(() => _isSyncing = true);
await widget.syncManager.syncNow();
if (mounted) {
setState(() => _isSyncing = false);
}
}
Future<void> _clearSyncedTasks() async {
await widget.syncManager.localStorage.clearSyncedTasks();
await _loadTasks();
}
@override
void dispose() {
unawaited(_syncSubscription.cancel());
_taskController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Offline Task Sync'),
actions: [
IconButton(
icon: const Icon(Icons.sync),
onPressed: _manualSync,
tooltip: 'Sync Now',
),
PopupMenuButton<TaskFilter>(
onSelected: (filter) {
setState(() {
_currentFilter = filter;
_loadTasks();
});
},
itemBuilder: (context) => [
const PopupMenuItem(
value: TaskFilter.all,
child: Text('All Tasks'),
),
const PopupMenuItem(
value: TaskFilter.synced,
child: Text('Synced'),
),
const PopupMenuItem(
value: TaskFilter.unsynced,
child: Text('Unsynced'),
),
],
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: _clearSyncedTasks,
tooltip: 'Clear Synced Tasks',
),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _taskController,
decoration: const InputDecoration(
labelText: 'Enter new task',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _addTask,
child: const Icon(Icons.add),
),
],
),
),
if (_isSyncing) const LinearProgressIndicator(),
if (_tasks.isEmpty)
const Expanded(
child: Center(
child: Text(
'No tasks available.\nAdd tasks to see them here!',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
),
)
else
Expanded(
child: ListView.builder(
itemCount: _tasks.length,
itemBuilder: (context, index) {
final task = _tasks[index];
final isCompleted = task.data['completed'] as bool;
final isSynced = task.status == SyncStatus.synced;
return ListTile(
leading: Icon(
isSynced ? Icons.cloud_done : Icons.cloud_off,
color: isSynced ? Colors.green : Colors.red,
),
title: Text(
task.data['title'],
style: TextStyle(
decoration: isCompleted
? TextDecoration.lineThrough
: null,
),
),
subtitle: Text(isSynced ? 'Synced' : 'Unsynced'),
trailing: Wrap(
children: [
IconButton(
icon: Icon(isCompleted ? Icons.undo : Icons.check),
onPressed: () => _toggleTask(task),
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _deleteTask(task),
),
],
),
);
},
),
),
Container(
padding: const EdgeInsets.all(12),
width: double.infinity,
color: Colors.blue.shade50,
child: Text(
'Status: $_syncStatus',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
),
],
),
);
}
}