smart_network 1.0.0 copy "smart_network: ^1.0.0" to clipboard
smart_network: ^1.0.0 copied to clipboard

A production-ready networking layer for Flutter with smart retries, advanced caching, request deduplication, offline support, automatic token refresh, batching, and seamless Dio integration.

example/lib/main.dart

// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
import 'package:smart_network/smart_network.dart';

// ─────────────────────────────────────────────────────────────────────────────
// Data models
// ─────────────────────────────────────────────────────────────────────────────

class Post {
  final int id;
  final String title;
  final String body;

  const Post({required this.id, required this.title, required this.body});

  factory Post.fromJson(dynamic json) {
    final map = json as Map<String, dynamic>;
    return Post(
      id: map['id'] as int,
      title: map['title'] as String,
      body: map['body'] as String,
    );
  }

  @override
  String toString() => 'Post(id: $id, title: "$title")';
}

// ─────────────────────────────────────────────────────────────────────────────
// TokenRefresher implementation (required by SmartConfig)
// ─────────────────────────────────────────────────────────────────────────────

/// Example implementation — replace with your real auth endpoint.
class ExampleTokenRefresher extends TokenRefresher {
  @override
  Future<TokenPair> refresh(String refreshToken) async {
    // In a real app you would POST to your /auth/refresh endpoint.
    // For the example, we simulate a successful refresh.
    await Future<void>.delayed(const Duration(milliseconds: 300));
    return const TokenPair(
      accessToken: 'fresh-access-token',
      refreshToken: 'fresh-refresh-token',
      expiresIn: 3600,
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Repository
// ─────────────────────────────────────────────────────────────────────────────

class PostRepository {
  final _client = SmartNetworkClient();

  /// GET /posts — cached, deduplicated, auto-retried
  Future<List<Post>> getPosts({int page = 1}) async {
    final res = await _client.get<List<Post>>(
      '/posts',
      queryParameters: {'_page': page, '_limit': 10},
      fromJson: (json) =>
          (json as List<dynamic>).map(Post.fromJson).toList(),
    );

    print('  fromCache: ${res.fromCache}  isStale: ${res.isStale}');
    return res.data;
  }

  /// GET /posts/1 — single post with long-lived cache
  Future<Post> getPost(int id) async {
    final res = await _client.get<Post>(
      '/posts/$id',
      fromJson: Post.fromJson,
    );
    return res.data;
  }

  /// POST /posts — queued when offline
  Future<void> createPost({
    required String title,
    required String body,
  }) async {
    final res = await _client.post<dynamic>(
      '/posts',
      data: {'title': title, 'body': body, 'userId': 1},
      queueIfOffline: true, // persisted when offline
    );

    if (res.isQueued) {
      print('  ✉️ Request queued — will be sent when online');
    } else {
      print('  ✅ Post created: ${res.data}');
    }
  }

  /// DELETE /posts/:id — queued when offline
  Future<void> deletePost(int id) async {
    await _client.delete<dynamic>(
      '/posts/$id',
      queueIfOffline: true,
    );
    print('  🗑️ Deleted post $id');
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// App entry point
// ─────────────────────────────────────────────────────────────────────────────

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // ── SmartNetwork one-time initialisation ──────────────────────────────────
  await SmartNetworkClient().initialize(
    SmartConfig(
      baseUrl: 'https://jsonplaceholder.typicode.com',

      // Timeouts
      connectTimeout: const Duration(seconds: 15),
      receiveTimeout: const Duration(seconds: 15),

      // Retry: up to 3 attempts with exponential back-off
      retryPolicy: const RetryPolicy(
        maxAttempts: 3,
        retryOnStatusCodes: {408, 500, 502, 503, 504},
        backoffStrategy: ExponentialBackoff(
          base: Duration(seconds: 1),
          maxDelay: Duration(seconds: 15),
          jitterFactor: 0.5,
        ),
      ),

      // Cache: stale-while-revalidate, 10-min TTL
      cachePolicy: const CachePolicy(
        enabled: true,
        maxAge: Duration(minutes: 10),
        staleAge: Duration(minutes: 30),
        strategy: CacheStrategy.staleWhileRevalidate,
        maxMemoryEntries: 200,
      ),

      // Auth (disabled for JSONPlaceholder — enable for your real API)
      // tokenRefresher: ExampleTokenRefresher(),

      // Offline queue
      enableOfflineMode: true,

      // Deduplication
      enableDeduplication: true,

      // Logging (disable in release builds)
      enableLogging: true,

      // Request batching (uncomment + add batchEndpoint to your API)
      // batchConfig: BatchConfig(
      //   batchEndpoint: '/batch',
      //   maxBatchSize: 10,
      //   windowDuration: Duration(milliseconds: 50),
      // ),
    ),
  );

  runApp(const SmartNetworkExampleApp());
}

// ─────────────────────────────────────────────────────────────────────────────
// UI
// ─────────────────────────────────────────────────────────────────────────────

class SmartNetworkExampleApp extends StatelessWidget {
  const SmartNetworkExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'smart_network Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const PostListPage(),
    );
  }
}

class PostListPage extends StatefulWidget {
  const PostListPage({super.key});

  @override
  State<PostListPage> createState() => _PostListPageState();
}

class _PostListPageState extends State<PostListPage> {
  final _repo = PostRepository();
  final _client = SmartNetworkClient();

  List<Post> _posts = [];
  bool _loading = false;
  String? _error;
  String _cacheStatus = '';

  @override
  void initState() {
    super.initState();
    _load();
  }

  Future<void> _load() async {
    setState(() {
      _loading = true;
      _error = null;
    });

    try {
      final posts = await _repo.getPosts();
      final diag = await _client.diagnostics();

      setState(() {
        _posts = posts;
        _loading = false;
        _cacheStatus =
            'Memory: ${diag['cacheEntries']['memory']} | '
            'Hive: ${diag['cacheEntries']['hive']} | '
            'Queue: ${diag['offlineQueueSize']}';
      });
    } on SmartException catch (e) {
      setState(() {
        _loading = false;
        _error = '${e.type.name}: ${e.message}';
      });
    }
  }

  Future<void> _create() async {
    try {
      await _repo.createPost(
        title: 'Test post ${DateTime.now().millisecond}',
        body: 'Created from smart_network example.',
      );
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Post created (or queued)!')),
      );
    } on SmartException catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Error: ${e.message}')),
      );
    }
  }

  Future<void> _clearCache() async {
    await _client.clearCache();
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Cache cleared')),
    );
    setState(() => _cacheStatus = 'Cache cleared');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('smart_network Demo'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        actions: [
          IconButton(
            icon: const Icon(Icons.delete_sweep),
            tooltip: 'Clear Cache',
            onPressed: _clearCache,
          ),
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: _load,
          ),
        ],
      ),
      body: Column(
        children: [
          // ── Cache status banner ──────────────────────────────────────────
          Container(
            width: double.infinity,
            color: Colors.indigo.shade50,
            padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
            child: Text(
              _cacheStatus.isEmpty ? 'Loading...' : '📦 Cache: $_cacheStatus',
              style: const TextStyle(fontSize: 12, color: Colors.indigo),
            ),
          ),

          // ── Content ──────────────────────────────────────────────────────
          Expanded(
            child: _loading
                ? const Center(child: CircularProgressIndicator())
                : _error != null
                    ? _ErrorView(message: _error!, onRetry: _load)
                    : _PostList(posts: _posts),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: _create,
        icon: const Icon(Icons.add),
        label: const Text('Create Post'),
      ),
    );
  }
}

// ── Sub-widgets ───────────────────────────────────────────────────────────────

class _PostList extends StatelessWidget {
  final List<Post> posts;
  const _PostList({required this.posts});

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemCount: posts.length,
      separatorBuilder: (_, __) => const Divider(height: 1),
      itemBuilder: (context, index) {
        final post = posts[index];
        return ListTile(
          leading: CircleAvatar(child: Text('${post.id}')),
          title: Text(
            post.title,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          ),
          subtitle: Text(
            post.body,
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
          ),
        );
      },
    );
  }
}

class _ErrorView extends StatelessWidget {
  final String message;
  final VoidCallback onRetry;

  const _ErrorView({required this.message, required this.onRetry});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Icon(Icons.error_outline, size: 48, color: Colors.red),
            const SizedBox(height: 12),
            Text(message, textAlign: TextAlign.center),
            const SizedBox(height: 20),
            FilledButton.icon(
              onPressed: onRetry,
              icon: const Icon(Icons.refresh),
              label: const Text('Retry'),
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
120
points
10
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A production-ready networking layer for Flutter with smart retries, advanced caching, request deduplication, offline support, automatic token refresh, batching, and seamless Dio integration.

Repository (GitHub)
View/report issues

Topics

#networking #http #dio #api-client #caching

License

MIT (license)

Dependencies

connectivity_plus, crypto, dio, equatable, flutter, hive, hive_flutter, logger, synchronized

More

Packages that depend on smart_network