Skip to main content

offline_first_image_box 0.0.1 copy "offline_first_image_box: ^0.0.1" to clipboard
offline_first_image_box: ^0.0.1 copied to clipboard

Resilient offline-first image loader for Flutter with progressive blur cross-fade, persistent caching, exponential retry backoff, and multi-tier fallbacks.

example/lib/main.dart

import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:offline_first_image_box/offline_first_image_box.dart';

void main() {
  runApp(const OfflineImageDemoApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Offline First Image Box Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF0284C7),
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      home: const ImageDemoHomePage(),
    );
  }
}

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

  @override
  State<ImageDemoHomePage> createState() => _ImageDemoHomePageState();
}

class _ImageDemoHomePageState extends State<ImageDemoHomePage> {
  bool _simulateOffline = false;
  final Map<String, ImageSource> _sourceMap = {};

  // Standard 67-byte valid 1x1 PNG image
  static final Uint8List _samplePng = Uint8List.fromList([
    0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
    0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
    0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00,
    0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00,
    0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49,
    0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
  ]);

  final List<String> _demoUrls = [
    'https://images.unsplash.com/photo-1',
    'https://images.unsplash.com/photo-2',
    'https://images.unsplash.com/photo-3',
    'https://images.unsplash.com/photo-4',
  ];

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

  void _configureMockFetcher() {
    ImageLoader.setCustomNetworkFetcher((uri) async {
      await Future.delayed(const Duration(milliseconds: 400));
      if (_simulateOffline) {
        throw Exception('Network unreachable (Simulated Offline Mode)');
      }
      return _samplePng;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Offline-First Image Loader'),
        backgroundColor: const Color(0xFF0284C7),
        foregroundColor: Colors.white,
        actions: [
          IconButton(
            icon: const Icon(Icons.delete_sweep_outlined),
            tooltip: 'Clear Cache',
            onPressed: () async {
              await OfflineImageCacheManager.instance.clear();
              setState(() {
                _sourceMap.clear();
              });
              if (context.mounted) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('Cache cleared!')),
                );
              }
            },
          ),
        ],
      ),
      body: Column(
        children: [
          // Offline Toggle Control Panel
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
            color: _simulateOffline
                ? const Color(0xFFFEF2F2)
                : const Color(0xFFF0FDF4),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Row(
                  children: [
                    Icon(
                      _simulateOffline
                          ? Icons.wifi_off_rounded
                          : Icons.wifi_rounded,
                      color: _simulateOffline
                          ? const Color(0xFFDC2626)
                          : const Color(0xFF16A34A),
                    ),
                    const SizedBox(width: 10),
                    Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          _simulateOffline ? 'Network: OFFLINE' : 'Network: ONLINE',
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            color: _simulateOffline
                                ? const Color(0xFFDC2626)
                                : const Color(0xFF16A34A),
                          ),
                        ),
                        Text(
                          _simulateOffline
                              ? 'Cached images will load; new ones will fail.'
                              : 'All images fetch from network or cache.',
                          style: const TextStyle(fontSize: 11, color: Colors.grey),
                        ),
                      ],
                    ),
                  ],
                ),
                Switch(
                  value: _simulateOffline,
                  activeTrackColor: const Color(0xFFDC2626),
                  onChanged: (val) {
                    setState(() {
                      _simulateOffline = val;
                      _configureMockFetcher();
                    });
                  },
                ),
              ],
            ),
          ),

          const Divider(height: 1),

          // Grid of Images
          Expanded(
            child: GridView.builder(
              padding: const EdgeInsets.all(16),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: 16,
                mainAxisSpacing: 16,
                childAspectRatio: 0.88,
              ),
              itemCount: _demoUrls.length,
              itemBuilder: (context, index) {
                final url = _demoUrls[index];
                final source = _sourceMap[url];

                return Card(
                  elevation: 2,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(16),
                  ),
                  clipBehavior: Clip.antiAlias,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      Expanded(
                        child: OfflineFirstImageBox(
                          key: ValueKey('$url-$_simulateOffline'),
                          imageUrl: url,
                          width: double.infinity,
                          height: double.infinity,
                          fit: BoxFit.cover,
                          onLoaded: (src) {
                            setState(() {
                              _sourceMap[url] = src;
                            });
                          },
                        ),
                      ),
                      Container(
                        padding: const EdgeInsets.all(8),
                        color: Colors.white,
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            Text(
                              'Photo #${index + 1}',
                              style: const TextStyle(
                                fontWeight: FontWeight.bold,
                                fontSize: 13,
                              ),
                            ),
                            if (source != null)
                              Container(
                                padding: const EdgeInsets.symmetric(
                                  horizontal: 6,
                                  vertical: 2,
                                ),
                                decoration: BoxDecoration(
                                  color: source == ImageSource.cache
                                      ? Colors.amber.shade100
                                      : Colors.blue.shade100,
                                  borderRadius: BorderRadius.circular(4),
                                ),
                                child: Text(
                                  source.name.toUpperCase(),
                                  style: TextStyle(
                                    fontSize: 10,
                                    fontWeight: FontWeight.bold,
                                    color: source == ImageSource.cache
                                        ? Colors.amber.shade900
                                        : Colors.blue.shade900,
                                  ),
                                ),
                              ),
                          ],
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}
1
likes
160
points
350
downloads

Documentation

API reference

Publisher

verified publisherbalochcodes.com

Weekly Downloads

Resilient offline-first image loader for Flutter with progressive blur cross-fade, persistent caching, exponential retry backoff, and multi-tier fallbacks.

Repository (GitHub)
View/report issues

Topics

#image #cache #offline #network #retry

License

MIT (license)

Dependencies

flutter

More

Packages that depend on offline_first_image_box