easy_rest_sync 0.0.2 copy "easy_rest_sync: ^0.0.2" to clipboard
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.

example/lib/main.dart

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}',
                ),
              );
            },
          ),
        ],
      ),
    );
  }
}
1
likes
140
points
28
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

dio, flutter, hive_ce_flutter, uuid

More

Packages that depend on easy_rest_sync