flutter_scratch_card 2.0.0 copy "flutter_scratch_card: ^2.0.0" to clipboard
flutter_scratch_card: ^2.0.0 copied to clipboard

A production-ready, highly customizable Flutter scratch card SDK with ImageProvider overlays, themes, brushes, haptics, multi-area scratching, persistence, and accessibility.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_scratch_card/flutter_scratch_card.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'flutter_scratch_card',
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: const Color(0xFF0F766E),
      ),
      home: const DemoHome(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final demos = <_DemoItem>[
      _DemoItem('Basic Scratch Card', (_) => const BasicDemo()),
      _DemoItem('Network Overlay', (_) => const NetworkOverlayDemo()),
      _DemoItem('Custom Brush', (_) => const CustomBrushDemo()),
      _DemoItem('Custom Shape', (_) => const CustomShapeDemo()),
      _DemoItem('Themes', (_) => const ThemesDemo()),
      _DemoItem('Haptics', (_) => const HapticsDemo()),
      _DemoItem('Sound (adapter)', (_) => const SoundDemo()),
      _DemoItem('Completion Effects', (_) => const CompletionEffectsDemo()),
      _DemoItem('Controller', (_) => const ControllerDemo()),
      _DemoItem('Multiple Scratch Areas', (_) => const MultiAreaDemo()),
      _DemoItem('Multiple Rewards', (_) => const MultiRewardDemo()),
      _DemoItem('Async Reward', (_) => const AsyncRewardDemo()),
      _DemoItem('Persistence', (_) => const PersistenceDemo()),
      _DemoItem('Accessibility', (_) => const AccessibilityDemo()),
      _DemoItem('Performance Demo', (_) => const PerformanceDemo()),
    ];

    return Scaffold(
      appBar: AppBar(title: const Text('Scratchify examples')),
      body: ListView.separated(
        itemCount: demos.length,
        separatorBuilder: (_, __) => const Divider(height: 1),
        itemBuilder: (context, index) {
          final item = demos[index];
          return ListTile(
            title: Text(item.title),
            trailing: const Icon(Icons.chevron_right),
            onTap: () => Navigator.of(context).push(
              MaterialPageRoute(
                builder: item.builder,
                settings: RouteSettings(name: item.title),
              ),
            ),
          );
        },
      ),
    );
  }
}

class _DemoItem {
  const _DemoItem(this.title, this.builder);
  final String title;
  final WidgetBuilder builder;
}

Widget _reward(String text) {
  return ColoredBox(
    color: const Color(0xFFFFF7ED),
    child: Center(
      child: Text(
        text,
        style: const TextStyle(
          fontSize: 36,
          fontWeight: FontWeight.w800,
          color: Color(0xFF9A3412),
        ),
      ),
    ),
  );
}

class _CardFrame extends StatelessWidget {
  const _CardFrame({required this.child, this.footer});

  final Widget child;
  final Widget? footer;

  @override
  Widget build(BuildContext context) {
    final title = ModalRoute.of(context)?.settings.name ?? 'Demo';
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Center(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(16),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SizedBox(
                width: 280,
                height: 280,
                child: DecoratedBox(
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(20),
                    boxShadow: const [
                      BoxShadow(
                        color: Color(0x33000000),
                        blurRadius: 18,
                        offset: Offset(0, 8),
                      ),
                    ],
                  ),
                  child: ClipRRect(
                    borderRadius: BorderRadius.circular(20),
                    child: child,
                  ),
                ),
              ),
              if (footer != null) ...[const SizedBox(height: 24), footer!],
            ],
          ),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      child: ScratchCard(
        overlayImage: const AssetImage('assets/scratch_texture.png'),
        threshold: 0.55,
        onComplete: () {},
        child: _reward('₹100'),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      child: ScratchCard(
        overlayImage: const NetworkImage(
          'https://picsum.photos/seed/scratch/400/400',
        ),
        fallbackOverlayImage: const AssetImage('assets/scratch_texture.png'),
        scratchColor: Colors.blueGrey,
        child: _reward('NET'),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      child: ScratchCard(
        brush: ScratchBrush.soft(size: 42),
        overlayImage: const AssetImage('assets/scratch_texture.png'),
        child: _reward('SOFT'),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      child: ScratchCard(
        revealShape: ScratchRevealShape.circle(),
        brush: ScratchBrush.circle(size: 36),
        scratchColor: const Color(0xFF64748B),
        child: _reward('◯'),
      ),
    );
  }
}

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

  @override
  State<ThemesDemo> createState() => _ThemesDemoState();
}

class _ThemesDemoState extends State<ThemesDemo> {
  var _index = 0;
  final _themes = [
    ScratchCardTheme.classic(),
    ScratchCardTheme.metallic(),
    ScratchCardTheme.golden(),
    ScratchCardTheme.premium(),
    ScratchCardTheme.neon(),
    ScratchCardTheme.minimal(),
  ];
  final _names = [
    'classic',
    'metallic',
    'golden',
    'premium',
    'neon',
    'minimal',
  ];

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      footer: Wrap(
        spacing: 8,
        children: [
          for (var i = 0; i < _names.length; i++)
            ChoiceChip(
              label: Text(_names[i]),
              selected: _index == i,
              onSelected: (_) => setState(() => _index = i),
            ),
        ],
      ),
      child: ScratchCard(
        key: ValueKey(_index),
        theme: _themes[_index],
        child: _reward(_names[_index].toUpperCase()),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      child: ScratchCard(
        haptic: const ScratchHapticConfig(
          enabled: true,
          onScratch: HapticType.selection,
          onMilestone: HapticType.medium,
          onComplete: HapticType.heavy,
        ),
        progressTriggers: const [0.3, 0.6],
        overlayImage: const AssetImage('assets/scratch_texture.png'),
        child: _reward('VIB'),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      footer: const Padding(
        padding: EdgeInsets.symmetric(horizontal: 24),
        child: Text(
          'Sound uses ScratchSoundPlayer — plug in audioplayers/just_audio in your app. This demo uses a no-op player.',
          textAlign: TextAlign.center,
        ),
      ),
      child: ScratchCard(
        sound: const ScratchSoundConfig(
          enabled: true,
          player: NoOpScratchSoundPlayer(),
        ),
        overlayImage: const AssetImage('assets/scratch_texture.png'),
        child: _reward('SND'),
      ),
    );
  }
}

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

  @override
  State<CompletionEffectsDemo> createState() => _CompletionEffectsDemoState();
}

class _CompletionEffectsDemoState extends State<CompletionEffectsDemo> {
  final _controller = ScratchController();
  var _effect = ScratchCompletionEffect.confetti;

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      footer: Column(
        children: [
          DropdownButton<ScratchCompletionEffect>(
            value: _effect,
            items: [
              for (final e in ScratchCompletionEffect.values)
                DropdownMenuItem(value: e, child: Text(e.name)),
            ],
            onChanged: (v) {
              if (v == null) return;
              setState(() => _effect = v);
              _controller.reset();
            },
          ),
          ElevatedButton(
            onPressed: _controller.reveal,
            child: const Text('Reveal'),
          ),
        ],
      ),
      child: ScratchCard(
        key: ValueKey(_effect),
        controller: _controller,
        completion: ScratchCompletionConfig(effect: _effect),
        overlayImage: const AssetImage('assets/scratch_texture.png'),
        child: _reward('FX'),
      ),
    );
  }
}

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

  @override
  State<ControllerDemo> createState() => _ControllerDemoState();
}

class _ControllerDemoState extends State<ControllerDemo> {
  final _controller = ScratchController();

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      footer: Wrap(
        spacing: 8,
        children: [
          ElevatedButton(
            onPressed: _controller.reveal,
            child: const Text('Reveal'),
          ),
          ElevatedButton(
            onPressed: () => _controller.reset(animated: true),
            child: const Text('Reset'),
          ),
          ElevatedButton(
            onPressed: _controller.pause,
            child: const Text('Pause'),
          ),
          ElevatedButton(
            onPressed: _controller.resume,
            child: const Text('Resume'),
          ),
        ],
      ),
      child: ScratchCard(
        controller: _controller,
        animationType: ScratchAnimationType.lottie,
        animationAsset: 'assets/Confetti.json',
        overlayImage: const AssetImage('assets/scratch_texture.png'),
        child: Image.asset('assets/reveal.jpg', fit: BoxFit.cover),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      child: ScratchCard(
        areas: [
          ScratchArea(
            id: 'a',
            flex: 1,
            child: _reward('A'),
            brush: ScratchBrush.hard(size: 28),
          ),
          ScratchArea(
            id: 'b',
            flex: 1,
            child: _reward('B'),
            brush: ScratchBrush.soft(size: 34),
          ),
        ],
        onAreaComplete: (id) => debugPrint('Area complete: $id'),
        scratchColor: const Color(0xFF94A3B8),
        child: const SizedBox.shrink(),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      child: ScratchCard(
        rewardSelection: ScratchRewardSelection.random,
        rewards: [
          ScratchReward(id: '50', child: _reward('₹50'), weight: 3),
          ScratchReward(id: '100', child: _reward('₹100'), weight: 2),
          ScratchReward(id: '500', child: _reward('₹500'), weight: 1),
        ],
        overlayImage: const AssetImage('assets/scratch_texture.png'),
        child: const SizedBox.shrink(),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      child: ScratchCard(
        rewardSelection: ScratchRewardSelection.async,
        rewardBuilder: (context) async {
          await Future<void>.delayed(const Duration(milliseconds: 800));
          return _reward('ASYNC');
        },
        rewardLoadingBuilder: (_) =>
            const Center(child: CircularProgressIndicator()),
        rewardErrorBuilder: (context, error, retry) => Center(
          child: TextButton(onPressed: retry, child: const Text('Retry')),
        ),
        overlayImage: const AssetImage('assets/scratch_texture.png'),
        child: const SizedBox.shrink(),
      ),
    );
  }
}

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

  @override
  State<PersistenceDemo> createState() => _PersistenceDemoState();
}

class _PersistenceDemoState extends State<PersistenceDemo> {
  final _storage = MemoryScratchStateStorage();
  final _controller = ScratchController();

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      footer: ElevatedButton(
        onPressed: () => _controller.reset(),
        child: const Text('Reset + clear storage'),
      ),
      child: ScratchCard(
        controller: _controller,
        stateKey: 'demo_persist',
        stateStorage: _storage,
        overlayImage: const AssetImage('assets/scratch_texture.png'),
        child: _reward('SAVE'),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      footer: const Padding(
        padding: EdgeInsets.symmetric(horizontal: 24),
        child: Text(
          'TalkBack / VoiceOver can activate the reveal semantic action.',
          textAlign: TextAlign.center,
        ),
      ),
      child: ScratchCard(
        semanticLabel: 'Scratch card containing a reward',
        semanticHint: 'Scratch to reveal your reward, or double-tap to reveal',
        overlayImage: const AssetImage('assets/scratch_texture.png'),
        child: _reward('A11Y'),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _CardFrame(
      child: ScratchCard(
        performance: ScratchPerformanceConfig.lowEnd(),
        brush: const ScratchBrush(size: 48, spacing: 0.35),
        overlayImage: const AssetImage('assets/scratch_texture.png'),
        child: _reward('PERF'),
      ),
    );
  }
}
5
likes
160
points
499
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A production-ready, highly customizable Flutter scratch card SDK with ImageProvider overlays, themes, brushes, haptics, multi-area scratching, persistence, and accessibility.

Repository (GitHub)
View/report issues

Topics

#scratch-card #ui #animation #rewards #flutter

License

MIT (license)

Dependencies

flutter, lottie

More

Packages that depend on flutter_scratch_card