easy_rest_sync 0.0.2
easy_rest_sync: ^0.0.2 copied to clipboard
An offline-first REST API synchronization package for Flutter with hidden local storage, reactive data, outbox queue, retry handling, cursor-based sync, and conflict management.
import 'dart:async';
import 'package:easy_rest_sync/easy_rest_sync.dart';
import 'package:flutter/material.dart';
late final EasyRestSync easySync;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
easySync = EasyRestSync(
config: EasyRestSyncConfig(
baseUrl: 'https://your-domain.com/api',
pushEndpoint: '/sync/push',
pullEndpoint: '/sync/pull',
scope: 'user_506618',
autoSyncOnWrite: true,
syncOnStart: true,
// Prevents the UI from rebuilding once for every record in a large pull.
streamDebounce: const Duration(milliseconds: 50),
// Easy REST Sync's own REST-based live streaming system.
streaming: const EasySyncStreamingConfig(
enabled: true,
interval: Duration(seconds: 10),
syncImmediately: true,
pauseWhenAppInBackground: true,
continueAfterError: true,
maxConsecutiveFailures: 0,
),
tokenProvider: () async {
// Return the current access token here.
return null;
},
conflictStrategy: EasySyncConflictStrategy.serverWins,
),
);
easySync.registerEntity<Product>(
EasySyncEntity<Product>(
name: 'products',
idSelector: (product) => product.id,
toJson: (product) => product.toJson(),
fromJson: Product.fromJson,
),
);
await easySync.initialize();
runApp(const EasySyncStreamingExample());
}
class Product {
final String id;
final String itemCode;
final String itemName;
final double quantity;
const Product({
required this.id,
required this.itemCode,
required this.itemName,
required this.quantity,
});
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id']?.toString() ?? '',
itemCode: json['itemCode']?.toString() ?? '',
itemName: json['itemName']?.toString() ?? '',
quantity: double.tryParse(json['quantity']?.toString() ?? '0') ?? 0,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'id': id,
'itemCode': itemCode,
'itemName': itemName,
'quantity': quantity,
};
}
}
class EasySyncStreamingExample extends StatelessWidget {
const EasySyncStreamingExample({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Easy REST Sync Streaming',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const ProductsPage(),
);
}
}
class ProductsPage extends StatefulWidget {
const ProductsPage({super.key});
@override
State<ProductsPage> createState() => _ProductsPageState();
}
class _ProductsPageState extends State<ProductsPage> {
final TextEditingController _nameController = TextEditingController();
final TextEditingController _quantityController = TextEditingController();
StreamSubscription<EasySyncEvent>? _eventSubscription;
String _lastEvent = 'No event yet';
@override
void initState() {
super.initState();
// Global event stream: local changes, remote changes, sync and streaming.
_eventSubscription = easySync.events.listen((event) {
if (!mounted) return;
setState(() {
if (event.isRecordChange) {
_lastEvent = '${event.origin?.name}: '
'${event.entity}/${event.recordId} '
'${event.recordChangeType?.name}';
} else {
_lastEvent = event.type.name;
}
});
});
}
@override
void dispose() {
_eventSubscription?.cancel();
_nameController.dispose();
_quantityController.dispose();
super.dispose();
}
Future<void> _addProduct() async {
final name = _nameController.text.trim();
final quantity = double.tryParse(_quantityController.text.trim());
if (name.isEmpty || quantity == null) return;
final id = DateTime.now().microsecondsSinceEpoch.toString();
await easySync.save<Product>(
entity: 'products',
data: Product(
id: id,
itemCode: 'ITEM-$id',
itemName: name,
quantity: quantity,
),
);
_nameController.clear();
_quantityController.clear();
}
Future<void> _toggleStreaming(EasySyncState state) async {
switch (state.streamingStatus) {
case EasySyncStreamingStatus.running:
await easySync.pauseStreaming();
return;
case EasySyncStreamingStatus.paused:
case EasySyncStreamingStatus.failed:
await easySync.resumeStreaming(syncNow: true);
return;
case EasySyncStreamingStatus.stopped:
await easySync.startStreaming(
config: const EasySyncStreamingConfig(
interval: Duration(seconds: 10),
syncImmediately: true,
),
);
return;
case EasySyncStreamingStatus.starting:
return;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Easy REST Sync Streaming'),
actions: <Widget>[
IconButton(
onPressed: () => unawaited(easySync.sync()),
tooltip: 'Sync now',
icon: const Icon(Icons.sync),
),
],
),
body: Column(
children: <Widget>[
StreamBuilder<EasySyncState>(
stream: easySync.watchState(),
builder: (context, snapshot) {
final state = snapshot.data ?? easySync.state;
return Card(
margin: const EdgeInsets.all(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Sync: ${state.status.name}'),
Text(
'Live stream: ${state.streamingStatus.name}',
),
Text('Pending: ${state.pendingOperations}'),
Text('Last event: $_lastEvent'),
],
),
),
IconButton.filledTonal(
onPressed: () => _toggleStreaming(state),
tooltip: state.isStreaming
? 'Pause streaming'
: 'Start or resume streaming',
icon: Icon(
state.isStreaming
? Icons.pause
: Icons.play_arrow,
),
),
IconButton.filledTonal(
onPressed: () => unawaited(easySync.stopStreaming()),
tooltip: 'Stop streaming',
icon: const Icon(Icons.stop),
),
],
),
),
);
},
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: <Widget>[
Expanded(
child: TextField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Product name',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
SizedBox(
width: 110,
child: TextField(
controller: _quantityController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Quantity',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
FilledButton(
onPressed: _addProduct,
child: const Text('Save'),
),
],
),
),
const SizedBox(height: 8),
Expanded(
child: StreamBuilder<List<Product>>(
// Emits immediately, after local writes and after remote pulls.
stream: easySync.watchList<Product>(
entity: 'products',
sort: (a, b) => a.itemName.compareTo(b.itemName),
),
builder: (context, snapshot) {
final products = snapshot.data ?? const <Product>[];
if (products.isEmpty) {
return const Center(child: Text('No products found'));
}
return ListView.separated(
itemCount: products.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.itemName),
subtitle: Text(product.itemCode),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(product.quantity.toString()),
IconButton(
onPressed: () => easySync.delete(
entity: 'products',
id: product.id,
),
icon: const Icon(Icons.delete_outline),
),
],
),
);
},
);
},
),
),
StreamBuilder<int>(
stream: easySync.watchPendingCount(),
builder: (context, snapshot) {
return Padding(
padding: const EdgeInsets.all(12),
child: Text(
'Pending operations: ${snapshot.data ?? 0}',
),
);
},
),
],
),
);
}
}