flutter_swipe_deck 1.2.1 copy "flutter_swipe_deck: ^1.2.1" to clipboard
flutter_swipe_deck: ^1.2.1 copied to clipboard

A customizable Tinder-style swipeable card deck for Flutter, with built-in pagination, controller-driven swipes, undo, drag overlays and four-way directions.

example/lib/main.dart

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

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

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

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

class Profile {
  const Profile(this.name, this.tagline, this.color);

  final String name;
  final String tagline;
  final Color color;
}

const _profiles = [
  Profile('Ramen crawl', 'Old town · 2 hours', Color(0xFFEF6C6C)),
  Profile('Rooftop cinema', 'Late showing · tonight', Color(0xFF6C7BEF)),
  Profile('Sunrise hike', 'Lookout trail · half day', Color(0xFF37A86B)),
  Profile('Pottery class', 'Craft studio · beginners', Color(0xFFE0A030)),
  Profile('Blanket fort', 'Home · board games', Color(0xFF8E5BD0)),
];

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

  @override
  State<DeckPage> createState() => _DeckPageState();
}

class _DeckPageState extends State<DeckPage> {
  final controller = SwipeDeckController();
  final liked = <String>[];
  final skipped = <String>[];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('flutter_swipe_deck'),
        actions: [
          IconButton(
            tooltip: 'Undo',
            onPressed: () => setState(controller.undo),
            icon: const Icon(Icons.undo_rounded),
          ),
          IconButton(
            tooltip: 'Paginated feed',
            onPressed: () => Navigator.of(
              context,
            ).push(MaterialPageRoute<void>(builder: (_) => const PagedPage())),
            icon: const Icon(Icons.all_inclusive_rounded),
          ),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(20),
              child: SwipeDeck<Profile>(
                items: _profiles,
                controller: controller,
                // Swipe up to super-like as well as left/right.
                allowedDirections: const [
                  SwipeDirection.left,
                  SwipeDirection.right,
                  SwipeDirection.up,
                ],
                backCardOffset: Offset(0, 24),
                itemBuilder: (context, profile, index) => _Card(profile),
                overlayBuilder: (context, direction, progress) {
                  return _Badge(direction: direction, progress: progress);
                },
                emptyBuilder: (context) =>
                    const Center(child: Text('That is everyone for now.')),
                onSwipe: (index, profile, direction) => setState(() {
                  if (direction == SwipeDirection.left) {
                    skipped.add(profile.name);
                  } else {
                    liked.add(profile.name);
                  }
                }),
                onUndo: (index, profile, direction) => setState(() {
                  liked.remove(profile.name);
                  skipped.remove(profile.name);
                }),
                onEnd: () => debugPrint('deck finished'),
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 20),
            child: Text('${liked.length} liked · ${skipped.length} skipped'),
          ),
          Padding(
            padding: const EdgeInsets.fromLTRB(20, 12, 20, 28),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                _RoundButton(
                  icon: Icons.close_rounded,
                  color: Colors.redAccent,
                  onPressed: controller.swipeLeft,
                ),
                _RoundButton(
                  icon: Icons.star_rounded,
                  color: Colors.blueAccent,
                  onPressed: controller.swipeUp,
                ),
                _RoundButton(
                  icon: Icons.favorite_rounded,
                  color: Colors.green,
                  onPressed: controller.swipeRight,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _Card extends StatelessWidget {
  const _Card(this.profile);

  final Profile profile;

  @override
  Widget build(BuildContext context) {
    return AspectRatio(
      aspectRatio: 1 / 1.5,
      child: Container(
        width: double.infinity,
        decoration: BoxDecoration(
          color: profile.color,
          borderRadius: BorderRadius.circular(28),
          boxShadow: const [
            BoxShadow(
              color: Colors.black26,
              blurRadius: 18,
              offset: Offset(0, 8),
            ),
          ],
        ),
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            Text(
              profile.name,
              style: const TextStyle(
                color: Colors.white,
                fontSize: 30,
                fontWeight: FontWeight.w700,
              ),
            ),
            const SizedBox(height: 6),
            Text(
              profile.tagline,
              style: const TextStyle(color: Colors.white70, fontSize: 16),
            ),
          ],
        ),
      ),
    );
  }
}

class _Badge extends StatelessWidget {
  const _Badge({required this.direction, required this.progress});

  final SwipeDirection direction;
  final double progress;

  @override
  Widget build(BuildContext context) {
    final (label, color, alignment) = switch (direction) {
      SwipeDirection.right => ('LIKE', Colors.green, Alignment.topLeft),
      SwipeDirection.left => ('NOPE', Colors.redAccent, Alignment.topRight),
      SwipeDirection.up => ('SUPER', Colors.blueAccent, Alignment.bottomCenter),
      SwipeDirection.down => ('LATER', Colors.orange, Alignment.topCenter),
    };

    return Opacity(
      opacity: progress,
      child: Align(
        alignment: alignment,
        child: Container(
          margin: const EdgeInsets.all(24),
          padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
          decoration: BoxDecoration(
            border: Border.all(color: color, width: 4),
            borderRadius: BorderRadius.circular(12),
          ),
          child: Text(
            label,
            style: TextStyle(
              color: color,
              fontSize: 26,
              fontWeight: FontWeight.w800,
              letterSpacing: 2,
            ),
          ),
        ),
      ),
    );
  }
}

class _RoundButton extends StatelessWidget {
  const _RoundButton({
    required this.icon,
    required this.color,
    required this.onPressed,
  });

  final IconData icon;
  final Color color;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return IconButton.filledTonal(
      onPressed: onPressed,
      iconSize: 32,
      padding: const EdgeInsets.all(16),
      icon: Icon(icon, color: color),
    );
  }
}

/// An endless feed loaded page by page.
class PagedPage extends StatelessWidget {
  const PagedPage({super.key});

  /// Stands in for a paginated API call.
  static Future<SwipeDeckPage<Profile>> _fetch(int page, Object? cursor) async {
    await Future<void>.delayed(const Duration(milliseconds: 700));
    return SwipeDeckPage(
      List.generate(10, (index) {
        final number = page * 10 + index + 1;
        final base = _profiles[number % _profiles.length];
        return Profile('${base.name} #$number', base.tagline, base.color);
      }),
      // Six pages, then the feed runs dry.
      hasMore: page < 5,
      nextCursor: 'page-$page',
    );
  }

  @override
  Widget build(BuildContext context) {
    final controller = SwipeDeckController();
    return Scaffold(
      appBar: AppBar(title: const Text('Paginated feed')),
      body: Column(
        children: [
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(20),
              child: PagedSwipeDeck<Profile>(
                controller: controller,
                fetcher: _fetch,
                pageSize: 10,
                backCardOffset: Offset(0, 18),
                prefetchThreshold: 4,
                // Never hold more than 40 cards, but keep 8 for undo.
                maxBufferedItems: 40,
                keepBehind: 8,
                itemBuilder: (context, profile, index) => _Card(profile),
                overlayBuilder: (context, direction, progress) =>
                    _Badge(direction: direction, progress: progress),
                loadingBuilder: (context) =>
                    const Center(child: CircularProgressIndicator()),
                errorBuilder: (context, error, retry) => Center(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Text('$error'),
                      const SizedBox(height: 12),
                      FilledButton(
                        onPressed: retry,
                        child: const Text('Retry'),
                      ),
                    ],
                  ),
                ),
                emptyBuilder: (context) => const Center(
                  child: Text('You reached the end of the feed.'),
                ),
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.fromLTRB(20, 0, 20, 28),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                _RoundButton(
                  icon: Icons.close_rounded,
                  color: Colors.redAccent,
                  onPressed: controller.swipeLeft,
                ),
                _RoundButton(
                  icon: Icons.undo_rounded,
                  color: Colors.blueGrey,
                  onPressed: controller.undo,
                ),
                _RoundButton(
                  icon: Icons.favorite_rounded,
                  color: Colors.green,
                  onPressed: controller.swipeRight,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
1
likes
155
points
126
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A customizable Tinder-style swipeable card deck for Flutter, with built-in pagination, controller-driven swipes, undo, drag overlays and four-way directions.

Repository (GitHub)
View/report issues

Topics

#cards #swipe #tinder #pagination #ui

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_swipe_deck