crash_safe_image 0.2.1 copy "crash_safe_image: ^0.2.1" to clipboard
crash_safe_image: ^0.2.1 copied to clipboard

Crash-safe image widget for Flutter. Auto-detects network/asset/file/memory sources, shows friendly placeholders & error UI, and uses cached_network_image for smart caching. Includes ImageProvider.

example/lib/main.dart

// example/lib/main.dart
import 'dart:convert'; // for base64 -> bytes
import 'dart:typed_data'; // for Uint8List
import 'dart:io' show File; // for File (non-web targets only)
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:crash_safe_image/crash_safe_image.dart';

void main() => runApp(const DemoApp());

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

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

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

  // Tiny 1x1 PNG (transparent) — just to demo bytes/memory
  Uint8List get tinyPngBytes => base64Decode(
    'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGMAAQAABQAB'
    'JwNfWQAAAABJRU5ErkJggg==',
  );

  @override
  Widget build(BuildContext context) {
    const goodUrl = 'https://picsum.photos/400/220';
    const badUrl = 'https://example.com/this-will-404.png';

    return Scaffold(
      appBar: AppBar(title: const Text('CrashSafeImage — Kitchen Sink')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _Section(
            title: '1) Network (good) + placeholder/error + fade + radius',
            child: ClipRRect(
              borderRadius: BorderRadius.circular(16),
              child: CrashSafeImage(
                goodUrl,
                width: double.infinity,
                height: 180,
                fit: BoxFit.cover,
                fadeInDuration: const Duration(milliseconds: 250),
                fadeOutDuration: const Duration(milliseconds: 250),
                placeholderBuilder: (_) => const _ShimmerishBox(),
                errorBuilder: (_) => _ErrorBox('Could not load image'),
              ),
            ),
          ),

          _Section(
            title: '2) Network (bad) — graceful error UI',
            child: SizedBox(
              width: double.infinity,
              height: 140,
              child: CrashSafeImage(
                badUrl,
                fit: BoxFit.cover,
                errorBuilder: (_) => _ErrorBox('404 / 400 → Fallback UI'),
              ),
            ),
          ),

          _Section(
            title: '3) With HTTP headers & custom cacheKey',
            child: SizedBox(
              width: double.infinity,
              height: 140,
              child: CrashSafeImage(
                goodUrl,
                cacheKey: 'hero-header:v1',
                httpHeaders: const {'Accept': 'image/*'},
                fit: BoxFit.cover,
                placeholderBuilder: (_) =>
                    const Center(child: CircularProgressIndicator()),
                errorBuilder: (_) => _ErrorBox('Header request failed'),
              ),
            ),
          ),

          _Section(
            title: '4) Memory (bytes) — tiny PNG',
            child: Row(
              children: [
                CrashSafeImage(
                  null,
                  bytes: tinyPngBytes,
                  width: 60,
                  height: 60,
                  color: Colors.indigo, // tint
                  colorBlendMode: BlendMode.srcATop, // blend
                ),
                const SizedBox(width: 12),
                const Expanded(
                  child: Text(
                    'A 1×1 transparent PNG rendered from memory bytes with tint.',
                  ),
                ),
              ],
            ),
          ),

          _Section(
            title: '5) Asset',
            child: Row(
              children: [
                CrashSafeImage(
                  'assets/logo.png', // add in example/pubspec.yaml
                  width: 80,
                  height: 80,
                  fit: BoxFit.contain,
                  errorBuilder: (_) =>
                      _ErrorBox('Missing asset? Add to pubspec.'),
                ),
                const SizedBox(width: 12),
                const Expanded(
                  child: Text(
                    'Asset image (remember to declare in example/pubspec.yaml).',
                  ),
                ),
              ],
            ),
          ),

          if (!kIsWeb)
            _Section(
              title: '6) File (non-web) — if file exists',
              child: FutureBuilder<bool>(
                future: _exampleFileExists(),
                builder: (context, snap) {
                  final exists = snap.data ?? false;
                  return Row(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      SizedBox(
                        width: 120,
                        height: 80,
                        child: exists
                            ? CrashSafeImage(
                                _exampleFilePath,
                                fit: BoxFit.cover,
                                errorBuilder: (_) =>
                                    _ErrorBox('Could not read file'),
                              )
                            : _ErrorBox('Demo file not found'),
                      ),
                      const SizedBox(width: 12),
                      Expanded(
                        child: SelectableText(
                          exists
                              ? 'Loaded from local file path:\n${_wrapForBreaks(_exampleFilePath)}'
                              : 'Place an image at:\n${_wrapForBreaks(_exampleFilePath)}',
                          //softWrap: true,
                        ),
                      ),
                    ],
                  );
                },
              ),
            ),

          _Section(
            title: '7) CircleAvatar — assertion-safe (provider is never null)',
            child: const Row(
              children: [
                DemoAvatar(url: 'https://i.pravatar.cc/150?img=5'),
                SizedBox(width: 16),
                DemoAvatar(url: 'https://example.com/does-not-exist.png'),
                SizedBox(width: 16),
                DemoAvatar(url: null), // API returned null → still safe
              ],
            ),
          ),

          _Section(
            title:
                '8) DecorationImage (Container bg) — safe fallback via onError',
            child: const SafeDecoratedBox(
              url:
                  'https://example.com/broken-bg.jpg', // will fallback to grey color
              height: 120,
              child: Center(child: Text('Fallback color if background fails')),
            ),
          ),

          _Section(
            title:
                '9) Stack background via widget (easiest full-control fallback)',
            child: SizedBox(
              height: 150,
              child: Stack(
                fit: StackFit.expand,
                children: [
                  CrashSafeImage(
                    goodUrl,
                    fit: BoxFit.cover,
                    placeholderBuilder: (_) => const _ShimmerishBox(),
                    errorBuilder: (_) => Container(color: Colors.grey.shade300),
                  ),
                  const Align(
                    alignment: Alignment.center,
                    child: Text(
                      'Overlay content',
                      style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.w600,
                        color: Colors.white,
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  // ---- File demo helpers (non-web) ----
  static String get _exampleFilePath {
    // For Android emulator, e.g. '/sdcard/Download/demo.jpg'
    // For macOS/iOS dev, set a valid path on your machine.
    return '/sdcard/Download/demo.jpg';
  }

  Future<bool> _exampleFileExists() async {
    try {
      final f = File(_exampleFilePath);
      return await f.exists();
    } catch (_) {
      return false;
    }
  }
}

// ---------- Widgets & Helpers ----------

class _Section extends StatelessWidget {
  const _Section({required this.title, required this.child});
  final String title;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 18),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            title,
            style: Theme.of(
              context,
            ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
          ),
          const SizedBox(height: 8),
          child,
        ],
      ),
    );
  }
}

class _ShimmerishBox extends StatelessWidget {
  const _ShimmerishBox();

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.grey.shade200,
      alignment: Alignment.center,
      child: const Text('Loading…'),
    );
  }
}

class _ErrorBox extends StatelessWidget {
  const _ErrorBox(this.message);
  final String message;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.grey.shade200,
      alignment: Alignment.center,
      child: Text(message, textAlign: TextAlign.center),
    );
  }
}

// ---- Helper for wrapping very long paths/URLs inside Row ----
String _wrapForBreaks(String s) => s.replaceAll('/', '/\u200B');

// ---- CircleAvatar demo using never-null provider ----
class DemoAvatar extends StatefulWidget {
  const DemoAvatar({super.key, required this.url});
  final String? url;

  @override
  State<DemoAvatar> createState() => _DemoAvatarState();
}

class _DemoAvatarState extends State<DemoAvatar> {
  bool _broken = false;

  @override
  Widget build(BuildContext context) {
    // provider is NEVER null now (transparent fallback on null/invalid/SVG)
    final img = CrashSafeImage(widget.url).provider;

    // Show overlay fallback icon when:
    //  - URL is null (API returned null), OR
    //  - actual fetch/decoding fails (onBackgroundImageError)
    final overlayNeeded = widget.url == null || _broken;

    return CircleAvatar(
      radius: 30,
      backgroundColor: Colors.grey.shade200,
      backgroundImage: img, // ← never null, so assertion-safe
      onBackgroundImageError: (exception, stack) {
        if (!mounted) return;
        setState(() => _broken = true);
      },
      child: overlayNeeded ? const Icon(Icons.person, size: 28) : null,
    );
  }
}

// ---- DecorationImage safe provider pattern ----
class SafeDecoratedBox extends StatefulWidget {
  const SafeDecoratedBox({
    super.key,
    required this.url,
    required this.height,
    this.child,
    this.borderRadius = 12,
  });

  final String url;
  final double height;
  final double borderRadius;
  final Widget? child;

  @override
  State<SafeDecoratedBox> createState() => _SafeDecoratedBoxState();
}

class _SafeDecoratedBoxState extends State<SafeDecoratedBox> {
  bool _bgBroken = false;

  @override
  Widget build(BuildContext context) {
    // provider is never null; onError swaps to fallback color
    final provider = CrashSafeImage(widget.url).provider;

    return Container(
      height: widget.height,
      clipBehavior: Clip.hardEdge,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(widget.borderRadius),
        color: _bgBroken ? Colors.grey.shade200 : null, // fallback color
        image: _bgBroken
            ? null
            : DecorationImage(
                image: provider,
                fit: BoxFit.cover,
                onError: (_, __) =>
                    mounted ? setState(() => _bgBroken = true) : null,
              ),
      ),
      child: widget.child,
    );
  }
}
1
likes
130
points
263
downloads

Publisher

verified publisherdevsloom.ca

Weekly Downloads

Crash-safe image widget for Flutter. Auto-detects network/asset/file/memory sources, shows friendly placeholders & error UI, and uses cached_network_image for smart caching. Includes ImageProvider.

Repository (GitHub)
View/report issues

Topics

#images #svg #widget #caching #utility

Documentation

API reference

License

MIT (license)

Dependencies

cached_network_image, flutter, flutter_svg

More

Packages that depend on crash_safe_image