fl_smart_http 1.0.1 copy "fl_smart_http: ^1.0.1" to clipboard
fl_smart_http: ^1.0.1 copied to clipboard

Flutter HTTP client with AES-256-GCM encrypted caching, offline support, priority request queue, interceptors, retry back-off, rate limiting, and DI support via IFlSmartHttp.

example/main.dart

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

// ══════════════════════════════════════════════════════════════════════════════
//  Model definitions — your own classes, zero package coupling
// ══════════════════════════════════════════════════════════════════════════════

class User {
  final int id;
  final String name;
  final String email;

  const User({required this.id, required this.name, required this.email});

  factory User.fromJson(dynamic json) => User(
        id: json['id'] as int,
        name: json['name'] as String,
        email: json['email'] as String,
      );

  @override
  String toString() => 'User(id: $id, name: $name, email: $email)';
}

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) => Post(
        id: json['id'] as int,
        title: json['title'] as String,
        body: json['body'] as String,
      );

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

// ══════════════════════════════════════════════════════════════════════════════
//  main() — initialise once, register models, run app
// ══════════════════════════════════════════════════════════════════════════════

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // ── 1. Initialise with baseUrl + interceptors ────────────────────────────
  await FlSmartHttp.init(
    baseUrl: 'https://jsonplaceholder.typicode.com',
    config: FlHttpConfig(
      defaultCacheDuration: const Duration(hours: 1),
      defaultRetryAttempts: 3,
      defaultStrategy: CacheStrategy.cacheFirst,
      serveStaleOnOffline: true,
      logLevel: FlLogLevel.verbose,
      // Interceptors are executed in order for every request/response
      interceptors: [
        LoggingInterceptor(), // pretty-prints every request & response
        // AuthRefreshInterceptor(
        //   onRefresh: () async => await authService.refreshToken(),
        // ),
      ],
      // Optional rate limiting
      maxRequestsPerSecond: 10,
      // Optional encryption — uncomment + store key securely in production:
      // enableEncryption: true,
      // encryptionKey: 'your-32-character-secret-key!!!!',
    ),
  );

  // ── 2. Register models once — no fromJson at call sites ──────────────────
  FlSmartHttp.registerModel<User>(User.fromJson);
  FlSmartHttp.registerModel<List<User>>(
    (json) => (json as List).map((e) => User.fromJson(e)).toList(),
  );
  FlSmartHttp.registerModel<Post>(Post.fromJson);
  FlSmartHttp.registerModel<List<Post>>(
    (json) => (json as List).map((e) => Post.fromJson(e)).toList(),
  );

  runApp(const MyApp());
}

// ══════════════════════════════════════════════════════════════════════════════
//  DI examples (pick ONE pattern for your app — they all work)
// ══════════════════════════════════════════════════════════════════════════════

// ── Provider ─────────────────────────────────────────────────────────────────
// MultiProvider(providers: [
//   Provider<IFlSmartHttp>.value(value: FlSmartHttp.instance),
// ]);
// Usage: context.read<IFlSmartHttp>().get<User>(...)

// ── Riverpod ──────────────────────────────────────────────────────────────────
// final httpProvider = FutureProvider<IFlSmartHttp>((ref) async {
//   final http = await FlSmartHttp.init(baseUrl: 'https://api.example.com');
//   ref.onDispose(() => http.dispose());
//   return http;
// });

// ── GetIt ─────────────────────────────────────────────────────────────────────
// GetIt.I.registerSingletonAsync<IFlSmartHttp>(
//   () => FlSmartHttp.init(baseUrl: 'https://api.example.com'),
// );
// await GetIt.I.allReady();

// ── Bloc ──────────────────────────────────────────────────────────────────────
// BlocProvider(create: (_) => UserBloc(context.read<IFlSmartHttp>()))

// ── Multiple isolated clients (multi-baseUrl) ─────────────────────────────────
// final authClient = await FlSmartHttp.create(
//   baseUrl: 'https://auth.example.com',
//   config: FlHttpConfig(hiveBoxName: 'auth_cache'),
// );
// GetIt.I.registerSingleton<IFlSmartHttp>(authClient, instanceName: 'auth');

// ══════════════════════════════════════════════════════════════════════════════
//  App
// ══════════════════════════════════════════════════════════════════════════════

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FlSmartHttp Demo',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const DemoScreen(),
    );
  }
}

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

  @override
  State<DemoScreen> createState() => _DemoScreenState();
}

class _DemoScreenState extends State<DemoScreen> {
  final _http = FlSmartHttp.instance;
  String _output = 'Tap a button to try a request';
  bool _loading = false;
  CancellationToken? _activeToken;

  // ── Example 1: GET with registered model ─────────────────────────────────
  Future<void> _getUser() async {
    _activeToken = CancellationToken();
    final res = await _http.get<User>(
      endpoint: '/users/1',
      // token: 'your_access_token', // uncomment when using a real API
      cacheResponse: true,
      retryPolicy: 3,
      cacheDuration: const Duration(minutes: 30),
      cancelToken: _activeToken,
    );
    setState(() {
      _output = res.isSuccess
          ? '✅ GET User\n'
              'Source: ${res.source.name}\n'
              'Elapsed: ${res.elapsed.inMilliseconds}ms\n'
              'Expires: ${res.expiresAt?.toLocal()}\n\n'
              'data → ${res.data}'
          : '❌ ${res.errorMessage}';
    });
  }

  // ── Example 2: GET list with registered model ─────────────────────────────
  Future<void> _getUsers() async {
    final res = await _http.get<List<User>>(
      endpoint: '/users',
      cacheResponse: true,
      retryPolicy: 2,
      cacheDuration: const Duration(hours: 1),
      tag: 'user-requests',           // used with cancelGroup('user-requests')
      priority: RequestPriority.high, // processed before normal/low requests
    );
    setState(() {
      _output = res.isSuccess
          ? '✅ GET Users (${res.data?.length} items)\n'
              'Source: ${res.source.name}\n'
              '${res.data?.take(3).join('\n')}'
          : '❌ ${res.errorMessage}';
    });
  }

  // ── Example 3: GET with query params ──────────────────────────────────────
  Future<void> _getPostsByUser() async {
    final res = await _http.get<List<Post>>(
      endpoint: '/posts',
      queryParams: {'userId': '1', '_limit': '5'},
      cacheResponse: true,
      cacheDuration: const Duration(minutes: 10),
    );
    setState(() {
      _output = res.isSuccess
          ? '✅ GET Posts (userId=1)\n'
              'Count: ${res.data?.length}\n'
              '${res.data?.map((p) => p.title).join('\n')}'
          : '❌ ${res.errorMessage}';
    });
  }

  // ── Example 4: GET WITHOUT model — returns raw body ───────────────────────
  // Use this when T is omitted or dynamic; data will be null but rawBody has content.
  Future<void> _getRaw() async {
    final res = await _http.get(
      endpoint: '/posts/1',
      cacheResponse: true,
    );
    setState(() {
      _output = res.isSuccess
          ? '✅ Raw GET\nSource: ${res.source.name}\n\n${res.rawBody}'
          : '❌ ${res.errorMessage}';
    });
  }

  // ── Example 5: GET with inline fromJson (no registration needed) ──────────
  Future<void> _getInlineFactory() async {
    final res = await _http.get<Post>(
      endpoint: '/posts/2',
      fromJson: Post.fromJson, // one-off override — skips the registry
      cacheResponse: true,
    );
    setState(() {
      _output = res.isSuccess
          ? '✅ Inline factory\n${res.data}'
          : '❌ ${res.errorMessage}';
    });
  }

  // ── Example 6: POST with body ─────────────────────────────────────────────
  Future<void> _createPost() async {
    final res = await _http.post<Post>(
      endpoint: '/posts',
      body: {
        'title': 'Flutter caching is easy',
        'body': 'Using fl_smart_http',
        'userId': 1,
      },
    );
    setState(() {
      _output = res.isSuccess
          ? '✅ POST Created\nStatus: ${res.statusCode}\n${res.data}'
          : '❌ ${res.errorMessage}';
    });
  }

  // ── Example 7: PUT (full update) ──────────────────────────────────────────
  Future<void> _updatePost() async {
    final res = await _http.put<Post>(
      endpoint: '/posts/1',
      body: {'title': 'Updated title', 'body': 'Updated body', 'userId': 1},
    );
    setState(() {
      _output = res.isSuccess
          ? '✅ PUT Updated\n${res.data}'
          : '❌ ${res.errorMessage}';
    });
  }

  // ── Example 8: PATCH (partial update) ────────────────────────────────────
  Future<void> _patchPost() async {
    final res = await _http.patch<Post>(
      endpoint: '/posts/1',
      body: {'title': 'Patched title only'},
    );
    setState(() {
      _output = res.isSuccess
          ? '✅ PATCH\nStatus: ${res.statusCode}\n${res.data}'
          : '❌ ${res.errorMessage}';
    });
  }

  // ── Example 9: DELETE + invalidate cache ─────────────────────────────────
  Future<void> _deletePost() async {
    final res = await _http.delete(endpoint: '/posts/1');
    await _http.invalidate('/posts/1'); // remove stale cache entry
    setState(() {
      _output = res.isSuccess
          ? '✅ DELETE ${res.statusCode} — cache invalidated'
          : '❌ ${res.errorMessage}';
    });
  }

  // ── Example 10: Stale-while-revalidate per request ────────────────────────
  // Returns cached data immediately (even if stale) and refreshes in background.
  Future<void> _swr() async {
    final res = await _http.get<List<Post>>(
      endpoint: '/posts',
      strategy: CacheStrategy.staleWhileRevalidate,
      cacheDuration: const Duration(seconds: 30),
      cacheResponse: true,
    );
    setState(() {
      _output = '✅ Stale-While-Revalidate\n'
          'Source: ${res.source.name}\n'
          'Is stale: ${res.isStale}\n'
          'Hit count: ${res.cacheHitCount}';
    });
  }

  // ── Example 11: Cancel active request ────────────────────────────────────
  void _cancelActive() {
    _activeToken?.cancel('User cancelled');
    setState(() => _output = '🚫 Request cancelled');
  }

  // ── Example 12: Cancel group ──────────────────────────────────────────────
  // Cancels all pending/active requests that were tagged 'user-requests'.
  void _cancelGroup() {
    _http.cancelGroup('user-requests');
    setState(() => _output = '🚫 Group "user-requests" cancelled');
  }

  // ── Example 13: Pause / resume queue ──────────────────────────────────────
  // Paused queue keeps requests pending; they run when resumed.
  void _pauseQueue() {
    _http.pauseQueue();
    setState(() => _output = '⏸ Queue paused — new requests will wait');
  }

  void _resumeQueue() {
    _http.resumeQueue();
    setState(() => _output = '▶ Queue resumed');
  }

  // ── Example 14: Cache prewarming ──────────────────────────────────────────
  // Seeds the cache with a known response before the first network call.
  Future<void> _prewarm() async {
    await _http.prewarm(
      '/config',
      '{"theme":"dark","version":"1.0","maintenance":false}',
      duration: const Duration(days: 7),
    );
    // Next get() for /config will be served instantly from cache
    final res = await _http.get(endpoint: '/config');
    setState(() {
      _output = '✅ Prewarmed + served from cache\n'
          'Source: ${res.source.name}\n'
          '${res.rawBody}';
    });
  }

  // ── Example 15: Cache invalidation ───────────────────────────────────────
  Future<void> _invalidate() async {
    await _http.invalidate('/users/1');           // single endpoint
    await _http.invalidateByPrefix('/posts');     // all /posts/* entries
    setState(() => _output = '✅ Invalidated /users/1 and all /posts/* entries');
  }

  // ── Example 16: Diagnostics ───────────────────────────────────────────────
  Future<void> _diagnostics() async {
    final d = _http.diagnostics();
    setState(() {
      _output = 'Diagnostics:\n'
          '${d.entries.map((e) => '  ${e.key}: ${e.value}').join('\n')}';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FlSmartHttp Demo'),
        backgroundColor: Colors.indigo,
        foregroundColor: Colors.white,
        actions: [
          // Real-time connectivity indicator
          StreamBuilder<bool>(
            stream: _http.connectivityStream,
            initialData: _http.isOnline,
            builder: (_, snap) => Padding(
              padding: const EdgeInsets.only(right: 12),
              child: Icon(
                snap.data! ? Icons.wifi : Icons.wifi_off,
                color: snap.data! ? Colors.greenAccent : Colors.redAccent,
              ),
            ),
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // Real-time queue status bar
            StreamBuilder<QueueStatus>(
              stream: _http.queueStatus,
              builder: (_, snap) {
                final s = snap.data;
                if (s == null) return const SizedBox.shrink();
                return Container(
                  width: double.infinity,
                  padding:
                      const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
                  margin: const EdgeInsets.only(bottom: 8),
                  decoration: BoxDecoration(
                    color: Colors.indigo.shade50,
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Text(
                    'Queue — pending: ${s.pending} | active: ${s.active} '
                    '| done: ${s.completed} | cancelled: ${s.cancelled}',
                    style: const TextStyle(fontSize: 11),
                  ),
                );
              },
            ),
            // Output panel
            Expanded(
              child: Container(
                width: double.infinity,
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: const Color(0xFF1E1E2E),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: _loading
                    ? const Center(child: CircularProgressIndicator())
                    : SingleChildScrollView(
                        child: Text(
                          _output,
                          style: const TextStyle(
                            color: Color(0xFFCDD6F4),
                            fontFamily: 'monospace',
                            fontSize: 12.5,
                          ),
                        ),
                      ),
              ),
            ),
            const SizedBox(height: 12),
            // Button grid
            Wrap(
              spacing: 8,
              runSpacing: 8,
              alignment: WrapAlignment.center,
              children: [
                _asyncBtn('GET User<T>', _getUser, Colors.green),
                _asyncBtn('GET List<T>', _getUsers, Colors.green),
                _asyncBtn('GET + QueryParams', _getPostsByUser, Colors.teal),
                _asyncBtn('GET Raw (no T)', _getRaw, Colors.blueGrey),
                _asyncBtn('GET Inline Factory', _getInlineFactory, Colors.cyan),
                _asyncBtn('POST + Body', _createPost, Colors.orange),
                _asyncBtn('PUT', _updatePost, Colors.deepOrange),
                _asyncBtn('PATCH', _patchPost, Colors.deepPurple),
                _asyncBtn('DELETE', _deletePost, Colors.red),
                _asyncBtn('Stale-While-Revalidate', _swr, Colors.purple),
                _btn('Cancel Active', _cancelActive, Colors.redAccent),
                _btn('Cancel Group', _cancelGroup, Colors.red.shade800),
                _btn('Pause Queue', _pauseQueue, Colors.amber),
                _btn('Resume Queue', _resumeQueue, Colors.lightGreen),
                _asyncBtn('Prewarm Cache', _prewarm, Colors.indigo),
                _asyncBtn('Invalidate Cache', _invalidate, Colors.brown),
                _asyncBtn('Diagnostics', _diagnostics, Colors.grey),
              ],
            ),
          ],
        ),
      ),
    );
  }

  /// Async button: shows loading spinner while the callback is running.
  Widget _asyncBtn(String label, Future<void> Function() onTap, Color color) =>
      ElevatedButton(
        onPressed: _loading
            ? null
            : () {
                setState(() {
                  _loading = true;
                  _output = '⏳ Loading...';
                });
                onTap().catchError((Object e) {
                  setState(() => _output = '❌ Error: $e');
                }).whenComplete(() => setState(() => _loading = false));
              },
        style: ElevatedButton.styleFrom(
          backgroundColor: color,
          foregroundColor: Colors.white,
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
          textStyle: const TextStyle(fontSize: 12),
        ),
        child: Text(label),
      );

  /// Sync button: for non-async operations (cancel, pause, resume).
  Widget _btn(String label, VoidCallback onTap, Color color) =>
      ElevatedButton(
        onPressed: onTap,
        style: ElevatedButton.styleFrom(
          backgroundColor: color,
          foregroundColor: Colors.white,
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
          textStyle: const TextStyle(fontSize: 12),
        ),
        child: Text(label),
      );
}
1
likes
160
points
86
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter HTTP client with AES-256-GCM encrypted caching, offline support, priority request queue, interceptors, retry back-off, rate limiting, and DI support via IFlSmartHttp.

Repository (GitHub)
View/report issues

Topics

#http #networking #caching #encryption #offline

Funding

Consider supporting this project:

github.com

License

MIT (license)

Dependencies

connectivity_plus, flutter, hive, hive_flutter, http, pointycastle

More

Packages that depend on fl_smart_http