simple_cached_future_builder 0.1.2 copy "simple_cached_future_builder: ^0.1.2" to clipboard
simple_cached_future_builder: ^0.1.2 copied to clipboard

A simplified FutureBuilder with basic caching abilities to not fetch new data on each rebuild.

example/lib/main.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:http/http.dart';
import 'package:simple_cached_future_builder/simple_cached_future_builder.dart';

/// Hive database used by [MyCustomCacheManager].
Box? database;

void main() async {
  await Hive.initFlutter();
  database = await Hive.openBox('database');
  runApp(const MyApp());
}

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

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

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _defaultManager = BasicCacheManager<String>();
  final _hiveManager = MyCustomCacheManager();

  /// Incremented by the Reload FAB — forces timed/no-cache rows to re-resolve.
  int _reloadKey = 0;

  /// Incremented by "Clear all" — forces ALL rows in that section to re-initialize.
  int _defaultClearKey = 0;
  int _hiveClearKey = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('SimpleCachedFutureBuilder'),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () => setState(() => _reloadKey++),
        icon: const Icon(Icons.refresh),
        label: const Text('Reload'),
        tooltip: 'Rebuild widget tree — cached rows serve instantly, '
            'uncached rows refetch',
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _InfoBanner(),
            const SizedBox(height: 16),
            _CacheSection(
              title: 'BasicCacheManager (default)',
              subtitle: 'In-memory — cleared on app restart',
              color: Colors.blue.shade50,
              manager: _defaultManager,
              tagPrefix: 'basic',
              reloadKey: _reloadKey,
              clearKey: _defaultClearKey,
              onClearAll: () => setState(() {
                _defaultManager.clearCache();
                _defaultClearKey++;
              }),
            ),
            const SizedBox(height: 16),
            _CacheSection(
              title: 'MyCustomCacheManager (Hive)',
              subtitle: 'Persistent — survives app restarts',
              color: Colors.green.shade50,
              manager: _hiveManager,
              tagPrefix: 'hive',
              reloadKey: _reloadKey,
              clearKey: _hiveClearKey,
              onClearAll: () => setState(() {
                _hiveManager.clearCache();
                _hiveClearKey++;
              }),
            ),
          ],
        ),
      ),
    );
  }
}

class _InfoBanner extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Card(
      color: Colors.amber.shade50,
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Text(
          'Tap Reload to simulate a parent widget rebuild:\n'
          '  • Permanent rows serve cached data immediately — no spinner.\n'
          '  • Timed rows re-check the cache; new data appears after the 10 s timer expires.\n'
          '  • No-cache rows always show a spinner and fetch fresh data.\n'
          'Hive cache survives app restarts; BasicCacheManager is in-memory only.',
          style: Theme.of(context).textTheme.bodySmall,
        ),
      ),
    );
  }
}

/// Displays one cache-manager section with three demo rows.
class _CacheSection extends StatefulWidget {
  final String title;
  final String subtitle;
  final Color color;
  final CacheManager<String> manager;
  final String tagPrefix;
  final int reloadKey;
  final int clearKey;
  final VoidCallback onClearAll;

  const _CacheSection({
    super.key,
    required this.title,
    required this.subtitle,
    required this.color,
    required this.manager,
    required this.tagPrefix,
    required this.reloadKey,
    required this.clearKey,
    required this.onClearAll,
  });

  @override
  State<_CacheSection> createState() => _CacheSectionState();
}

class _CacheSectionState extends State<_CacheSection> {
  /// Incremented when the permanent row's individual delete button is pressed,
  /// forcing that row to rebuild and refetch.
  int _permanentDeleteKey = 0;

  static Future<String> _fetch() =>
      get(Uri.parse('https://bored-api.appbrewery.com/random'))
          .then((r) => jsonDecode(r.body)['activity'] as String);

  @override
  Widget build(BuildContext context) {
    final timedCache = SimpleCache(
      tag: '${widget.tagPrefix}_timed',
      validFor: const Duration(seconds: 10),
    );
    final permanentCache =
        SimpleCache(tag: '${widget.tagPrefix}_permanent');

    return Card(
      color: widget.color,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // — Section header —
            Row(
              children: [
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(widget.title,
                          style: Theme.of(context)
                              .textTheme
                              .titleSmall
                              ?.copyWith(fontWeight: FontWeight.bold)),
                      Text(widget.subtitle,
                          style: Theme.of(context)
                              .textTheme
                              .bodySmall
                              ?.copyWith(color: Colors.grey.shade700)),
                    ],
                  ),
                ),
                TextButton.icon(
                  onPressed: widget.onClearAll,
                  icon: const Icon(Icons.clear_all, size: 18),
                  label: const Text('Clear all'),
                ),
              ],
            ),
            const Divider(),

            // Permanent — key changes on "Clear all" OR individual delete,
            // forcing the row to rebuild and refetch in both cases.
            _CacheRow(
              key: ValueKey(
                  '${widget.tagPrefix}_permanent_${widget.clearKey}_$_permanentDeleteKey'),
              label: 'Permanent',
              future: _fetch(),
              cache: permanentCache,
              manager: widget.manager,
              onDeleted: () => setState(() => _permanentDeleteKey++),
            ),
            const Divider(height: 8),

            // Timed — key changes on every Reload so the widget re-initializes
            // and re-checks whether the 10 s cache is still valid.
            _CacheRow(
              key: ValueKey(
                  '${widget.tagPrefix}_timed_${widget.reloadKey}_${widget.clearKey}'),
              label: 'Timed (10 s)',
              future: _fetch(),
              cache: timedCache,
              manager: widget.manager,
            ),
            const Divider(height: 8),

            // No cache — always re-fetches on Reload.
            _CacheRow(
              key: ValueKey(
                  '${widget.tagPrefix}_nocache_${widget.reloadKey}_${widget.clearKey}'),
              label: 'No cache',
              future: _fetch(),
              cache: null,
              manager: widget.manager,
            ),
          ],
        ),
      ),
    );
  }
}

/// A single labelled row wrapping [SimpleCachedFutureBuilder].
class _CacheRow extends StatelessWidget {
  final String label;
  final Future<String> future;
  final SimpleCache? cache;
  final CacheManager<String> manager;
  final VoidCallback? onDeleted;

  const _CacheRow({
    super.key,
    required this.label,
    required this.future,
    required this.cache,
    required this.manager,
    this.onDeleted,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6),
      child: Row(
        children: [
          SizedBox(
            width: 96,
            child: Text(label, style: Theme.of(context).textTheme.labelMedium),
          ),
          Expanded(
            child: SimpleCachedFutureBuilder<String>(
              future: future,
              builder: (context, activity) =>
                  Text(activity, style: Theme.of(context).textTheme.bodySmall),
              onLoadingWidget: const SizedBox(
                width: 14,
                height: 14,
                child: CircularProgressIndicator(strokeWidth: 2),
              ),
              onErrorWidget: (e) => const Icon(Icons.warning_amber, size: 16),
              cache: cache,
              cacheManager: cache != null ? manager : null,
            ),
          ),
          if (cache != null)
            _TimerWidget(
              cache: cache!,
              manager: manager,
              onDeleted: onDeleted,
            ),
        ],
      ),
    );
  }
}

/// Shows the remaining cache time and a delete button for a single entry.
class _TimerWidget extends StatelessWidget {
  final SimpleCache cache;
  final CacheManager<String> manager;
  final VoidCallback? onDeleted;

  const _TimerWidget({
    required this.cache,
    required this.manager,
    this.onDeleted,
  });

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<Duration?>(
      stream: manager.stream(cache),
      builder: (context, snapshot) {
        final secs = snapshot.data?.inSeconds;
        return Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            SizedBox(
              width: 28,
              child: Text(
                secs != null && secs > 0 ? '${secs}s' : '',
                style: Theme.of(context).textTheme.labelSmall,
                textAlign: TextAlign.right,
              ),
            ),
            IconButton(
              icon: const Icon(Icons.delete_outline, size: 18),
              tooltip: 'Remove this cache entry',
              padding: EdgeInsets.zero,
              constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
              onPressed: () {
                manager.removeCache(cache);
                onDeleted?.call();
              },
            ),
          ],
        );
      },
    );
  }
}

/// Hive-backed [CacheManager] — values survive app restarts.
class MyCustomCacheManager extends CacheManager<String> {
  @override
  void clearCache() {
    super.clearCache();
    database?.clear();
  }

  @override
  Future<bool> exists(SimpleCache tag) async =>
      database?.containsKey(tag.tag) ?? false;

  @override
  void removeCache(SimpleCache tag) {
    super.removeCache(tag);
    database?.delete(tag.tag);
  }

  @override
  String retrieveCache(SimpleCache tag) =>
      database?.get(tag.tag, defaultValue: '');

  @override
  void storeCache(SimpleCache tag, String data) => database?.put(tag.tag, data);
}
3
likes
150
points
100
downloads
screenshot

Documentation

API reference

Publisher

verified publisherapps.arvidnystrom.se

Weekly Downloads

A simplified FutureBuilder with basic caching abilities to not fetch new data on each rebuild.

Repository (GitHub)
View/report issues

Topics

#network #futurebuilder #cache

License

MIT (license)

Dependencies

flutter

More

Packages that depend on simple_cached_future_builder