easy_rest_sync 0.0.1
easy_rest_sync: ^0.0.1 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 '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: false,
syncOnStart: false,
tokenProvider: () async {
// Return your login 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 EasySyncExampleApp());
}
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,
};
}
Product copyWith({
String? id,
String? itemCode,
String? itemName,
double? quantity,
}) {
return Product(
id: id ?? this.id,
itemCode: itemCode ?? this.itemCode,
itemName: itemName ?? this.itemName,
quantity: quantity ?? this.quantity,
);
}
}
class EasySyncExampleApp extends StatelessWidget {
const EasySyncExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Easy REST Sync Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const ProductPage(),
);
}
}
class ProductPage extends StatefulWidget {
const ProductPage({super.key});
@override
State<ProductPage> createState() => _ProductPageState();
}
class _ProductPageState extends State<ProductPage> {
final TextEditingController _itemCodeController = TextEditingController();
final TextEditingController _itemNameController = TextEditingController();
final TextEditingController _quantityController = TextEditingController();
String? _editingId;
bool _saving = false;
String? _message;
@override
void dispose() {
_itemCodeController.dispose();
_itemNameController.dispose();
_quantityController.dispose();
super.dispose();
}
Future<void> _saveProduct() async {
final itemCode = _itemCodeController.text.trim();
final itemName = _itemNameController.text.trim();
final quantity = double.tryParse(_quantityController.text.trim());
if (itemCode.isEmpty || itemName.isEmpty || quantity == null) {
setState(() {
_message = 'Item code, item name and valid quantity are required.';
});
return;
}
setState(() {
_saving = true;
_message = null;
});
try {
final product = Product(
id: _editingId ?? DateTime.now().microsecondsSinceEpoch.toString(),
itemCode: itemCode,
itemName: itemName,
quantity: quantity,
);
await easySync.save<Product>(
entity: 'products',
data: product,
);
_clearForm();
setState(() {
_message = 'Saved locally. Press Sync to send it to the server.';
});
} catch (error) {
setState(() {
_message = error.toString();
});
} finally {
if (mounted) {
setState(() {
_saving = false;
});
}
}
}
void _editProduct(Product product) {
setState(() {
_editingId = product.id;
_itemCodeController.text = product.itemCode;
_itemNameController.text = product.itemName;
_quantityController.text = product.quantity.toString();
_message = null;
});
}
Future<void> _deleteProduct(Product product) async {
await easySync.delete(
entity: 'products',
id: product.id,
);
if (_editingId == product.id) {
_clearForm();
}
}
Future<void> _syncNow() async {
final result = await easySync.sync();
if (!mounted) return;
setState(() {
_message = result.isSuccess
? 'Sync complete: ${result.pushedCount} pushed, '
'${result.pulledCount} pulled, '
'${result.pendingCount} pending.'
: 'Sync failed: ${result.message}';
});
}
void _clearForm() {
_editingId = null;
_itemCodeController.clear();
_itemNameController.clear();
_quantityController.clear();
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Easy REST Sync'),
actions: <Widget>[
StreamBuilder<EasySyncStatus>(
stream: easySync.statusStream,
initialData: easySync.status,
builder: (context, snapshot) {
final status = snapshot.data ?? EasySyncStatus.idle;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Center(child: Text(status.name)),
);
},
),
IconButton(
onPressed: _syncNow,
tooltip: 'Sync now',
icon: const Icon(Icons.sync),
),
],
),
body: SafeArea(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: <Widget>[
TextField(
controller: _itemCodeController,
decoration: const InputDecoration(
labelText: 'Item code',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextField(
controller: _itemNameController,
decoration: const InputDecoration(
labelText: 'Item name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextField(
controller: _quantityController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: const InputDecoration(
labelText: 'Quantity',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
Row(
children: <Widget>[
Expanded(
child: FilledButton.icon(
onPressed: _saving ? null : _saveProduct,
icon: const Icon(Icons.save),
label: Text(
_editingId == null ? 'Save' : 'Update',
),
),
),
if (_editingId != null) ...<Widget>[
const SizedBox(width: 10),
OutlinedButton(
onPressed: _clearForm,
child: const Text('Cancel'),
),
],
],
),
if (_message != null) ...<Widget>[
const SizedBox(height: 10),
Align(
alignment: Alignment.centerLeft,
child: Text(_message!),
),
],
],
),
),
const Divider(height: 1),
Expanded(
child: StreamBuilder<List<Product>>(
stream: easySync.watchList<Product>(
entity: 'products',
sort: (a, b) => a.itemName.compareTo(b.itemName),
),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
final products = snapshot.data ?? const <Product>[];
if (products.isEmpty) {
return const Center(child: Text('No local product found.'));
}
return ListView.separated(
itemCount: products.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
onTap: () => _editProduct(product),
title: Text(product.itemName),
subtitle: Text(
'Code: ${product.itemCode} | Qty: ${product.quantity}',
),
trailing: IconButton(
onPressed: () => _deleteProduct(product),
icon: const Icon(Icons.delete_outline),
),
);
},
);
},
),
),
],
),
),
);
}
}