bedrock_sync 0.1.0
bedrock_sync: ^0.1.0 copied to clipboard
A backend-agnostic, offline-first sync engine for Flutter. Your data stays grounded on device and syncs to any backend — REST, Firebase, or Supabase — when online.
import 'package:flutter/material.dart';
import 'package:bedrock_sync/bedrock_sync.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// ─── Initialize BedrockSync ────────────────────────────────────────────
final engine = BedrockEngine(
adapter: RestSyncAdapter(
baseUrl: 'https://jsonplaceholder.typicode.com',
// Customize URL building to fit your API
buildUrl: (collection, id) => id != null ? '/todos/$id' : '/todos',
),
config: SyncConfig(
enableLogging: true, // show logs in debug
maxRetries: 3, // retry 3 times before failing
retryStrategy: RetryStrategy.exponential, // 2s, 4s, 8s...
syncTrigger: SyncTrigger.hybrid, // sync on reconnect + interval
syncInterval: const Duration(minutes: 5),
conflictResolver: LastWriteWinsResolver(), // swap to any resolver
onAfterSync: (count) => debugPrint('✅ Synced $count operations'),
onError: (e, stack) => debugPrint('❌ Sync error: $e'),
),
);
await engine.init();
runApp(MyApp(engine: engine));
}
class MyApp extends StatelessWidget {
final BedrockEngine engine;
const MyApp({super.key, required this.engine});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BedrockSync Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: TodoScreen(engine: engine),
);
}
}
// ─── Todo Screen ─────────────────────────────────────────────────────────────
class TodoScreen extends StatefulWidget {
final BedrockEngine engine;
const TodoScreen({super.key, required this.engine});
@override
State<TodoScreen> createState() => _TodoScreenState();
}
class _TodoScreenState extends State<TodoScreen> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _addTodo() async {
final title = _controller.text.trim();
if (title.isEmpty) return;
await widget.engine.write('todos', {
'title': title,
'done': false,
'updatedAt': DateTime.now().toIso8601String(),
});
_controller.clear();
}
Future<void> _toggleTodo(Map<String, dynamic> todo) async {
await widget.engine.updateRecord(
'todos',
todo['_id'] as String,
{
...todo,
'done': !(todo['done'] as bool? ?? false),
'updatedAt': DateTime.now().toIso8601String(),
},
);
}
Future<void> _deleteTodo(String id) async {
await widget.engine.deleteRecord('todos', id);
}
Future<void> _manualSync() async {
final result = await widget.engine.sync();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
result.success
? '✅ Synced ${result.syncedCount} items in ${result.duration.inMilliseconds}ms'
: '❌ Sync failed: ${result.errors.first.message}',
),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('BedrockSync Demo'),
actions: [
// Sync status indicator
StreamBuilder<SyncStatus>(
stream: widget.engine.statusStream,
builder: (context, snapshot) {
final status = snapshot.data;
return Padding(
padding: const EdgeInsets.only(right: 12),
child: Row(
children: [
Icon(
status?.isOnline == true
? Icons.cloud_done
: Icons.cloud_off,
color:
status?.isOnline == true ? Colors.green : Colors.red,
),
if ((status?.pendingCount ?? 0) > 0)
Badge(
label: Text('${status!.pendingCount}'),
child: const Icon(Icons.sync),
),
],
),
);
},
),
],
),
body: Column(
children: [
// Status Banner
StreamBuilder<SyncStatus>(
stream: widget.engine.statusStream,
builder: (context, snapshot) {
final status = snapshot.data;
if (status == null) return const SizedBox.shrink();
Color color;
String label;
switch (status.state) {
case SyncState.syncing:
color = Colors.blue.shade100;
label = '🔄 Syncing...';
break;
case SyncState.synced:
color = Colors.green.shade100;
label = '✅ All synced';
break;
case SyncState.offline:
color = Colors.orange.shade100;
label = '📵 Offline — changes saved locally';
break;
case SyncState.error:
color = Colors.red.shade100;
label = '❌ Sync error: ${status.lastError}';
break;
default:
color = Colors.grey.shade100;
label = status.pendingCount > 0
? '⏳ ${status.pendingCount} pending'
: 'Ready';
}
return Container(
width: double.infinity,
color: color,
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(label, style: const TextStyle(fontSize: 13)),
);
},
),
// Add Todo Input
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Add a todo...',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _addTodo(),
),
),
const SizedBox(width: 8),
FilledButton(
onPressed: _addTodo,
child: const Text('Add'),
),
],
),
),
// Todo List — streams local changes in real-time
Expanded(
child: StreamBuilder<List<Map<String, dynamic>>>(
stream: widget.engine.watch('todos'),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final todos = snapshot.data!;
if (todos.isEmpty) {
return const Center(
child: Text('No todos yet. Add one above!'),
);
}
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
final done = todo['done'] as bool? ?? false;
return ListTile(
leading: Checkbox(
value: done,
onChanged: (_) => _toggleTodo(todo),
),
title: Text(
todo['title']?.toString() ?? '',
style: TextStyle(
decoration: done ? TextDecoration.lineThrough : null,
color: done ? Colors.grey : null,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _deleteTodo(todo['_id'] as String),
),
);
},
);
},
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _manualSync,
icon: const Icon(Icons.sync),
label: const Text('Sync Now'),
),
);
}
}