Creare uno swipe di card in stile Tinder in Flutter con flutter_card_swiper
GuideIntermedio35 min Flutter 3.x

Creare uno swipe di card in stile Tinder in Flutter con flutter_card_swiper

Card swipe in stile Tinder

Le interfacce a stack di card con swipe direzionale sono diventate popolari grazie ad app come Tinder, ma sono utili in molti contesti: onboarding, selezione di preferenze, quiz, cataloghi prodotti.

In questo tutorial useremo il pacchetto flutter_card_swiper, una soluzione leggera e senza dipendenze native, per costruire uno stack di card che l'utente può trascinare a destra (like), a sinistra (dislike) o in alto/basso.

Alla fine avremo un widget riutilizzabile con pulsanti di azione, callback sugli eventi di swipe e gestione della fine dello stack.

  1. 1

    Aggiungere la dipendenza flutter_card_swiper

    Apri il file pubspec.yaml e aggiungi il pacchetto flutter_card_swiper nella sezione delle dipendenze. Poi esegui il comando di installazione.

    flutter pub add flutter_card_swiper
    

    Questo comando aggiunge automaticamente la voce corretta in pubspec.yaml e scarica il pacchetto.

    dependencies:
      flutter:
        sdk: flutter
      flutter_card_swiper: ^7.0.0

    Risultato atteso

    Il pacchetto viene scaricato senza errori e risulta disponibile per l'import.

  2. 2

    Preparare il modello dati delle card

    Creiamo un semplice modello ProfileCard che rappresenta il contenuto di ogni card. In un'app reale questi dati arriverebbero da un backend, ma qui usiamo una lista statica.

    class ProfileCard {
      final String name;
      final int age;
      final String imageUrl;
      final Color color;
    
      const ProfileCard({
        required this.name,
        required this.age,
        required this.imageUrl,
        required this.color,
      });
    }
    
    final demoCards = <ProfileCard>[
      const ProfileCard(name: 'Alice', age: 27, imageUrl: 'https://picsum.photos/id/64/400/600', color: Colors.pinkAccent),
      const ProfileCard(name: 'Marco', age: 31, imageUrl: 'https://picsum.photos/id/91/400/600', color: Colors.blueAccent),
      const ProfileCard(name: 'Giulia', age: 24, imageUrl: 'https://picsum.photos/id/823/400/600', color: Colors.deepPurpleAccent),
      const ProfileCard(name: 'Luca', age: 29, imageUrl: 'https://picsum.photos/id/1005/400/600', color: Colors.tealAccent),
    ];

    Risultato atteso

    Hai una lista di 4 card pronte da mostrare nello stack.

  3. 3

    Costruire il widget della singola card

    Isoliamo la UI di una card in un widget dedicato per mantenere il codice pulito. Mostriamo un'immagine di sfondo con un gradiente scuro in basso e le informazioni del profilo.

    class ProfileCardView extends StatelessWidget {
      final ProfileCard card;
      const ProfileCardView({super.key, required this.card});
    
      @override
      Widget build(BuildContext context) {
        return ClipRRect(
          borderRadius: BorderRadius.circular(24),
          child: Stack(
            fit: StackFit.expand,
            children: [
              Image.network(card.imageUrl, fit: BoxFit.cover,
                errorBuilder: (_, __, ___) => Container(color: card.color)),
              const DecoratedBox(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.center,
                    end: Alignment.bottomCenter,
                    colors: [Colors.transparent, Colors.black87],
                  ),
                ),
              ),
              Positioned(
                left: 20,
                bottom: 24,
                child: Text('${card.name}, ${card.age}',
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 26,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ],
          ),
        );
      }
    }

    Risultato atteso

    Una card con immagine, sfumatura e nome/età sovrapposti in basso a sinistra.

  4. 4

    Inserire il CardSwiper nella schermata

    Ora usiamo il widget CardSwiper. Ha bisogno di:

    • cardsCount: il numero di card
    • cardBuilder: una funzione che costruisce la card all'indice richiesto
    • un CardSwiperController per controllarlo programmaticamente (utile per i pulsanti nel passo successivo)

    Ricorda di dichiarare il controller in uno StatefulWidget e di rilasciarlo nel dispose.

    class SwipeScreen extends StatefulWidget {
      const SwipeScreen({super.key});
    
      @override
      State<SwipeScreen> createState() => _SwipeScreenState();
    }
    
    class _SwipeScreenState extends State<SwipeScreen> {
      final CardSwiperController _controller = CardSwiperController();
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Scopri profili')),
          body: Padding(
            padding: const EdgeInsets.all(16),
            child: CardSwiper(
              controller: _controller,
              cardsCount: demoCards.length,
              numberOfCardsDisplayed: 3,
              backCardOffset: const Offset(0, 40),
              padding: const EdgeInsets.all(24),
              cardBuilder: (context, index, percentX, percentY) {
                return ProfileCardView(card: demoCards[index]);
              },
            ),
          ),
        );
      }
    }

    Risultato atteso

    Vedi uno stack di card trascinabili con effetto di profondità sulle card retrostanti.

  5. 5

    Gestire gli eventi di swipe

    Il callback onSwipe viene invocato ad ogni swipe completato e riceve la direzione (CardSwiperDirection.left, right, top, bottom). Restituendo true confermiamo lo swipe, con false lo annulliamo (utile per condizioni di blocco).

    Usiamo anche onEnd per sapere quando lo stack è terminato.

    CardSwiper(
      controller: _controller,
      cardsCount: demoCards.length,
      numberOfCardsDisplayed: 3,
      onSwipe: (previousIndex, currentIndex, direction) {
        final card = demoCards[previousIndex];
        switch (direction) {
          case CardSwiperDirection.right:
            debugPrint('LIKE a ${card.name}');
            break;
          case CardSwiperDirection.left:
            debugPrint('NOPE a ${card.name}');
            break;
          default:
            debugPrint('Swipe ${direction.name} su ${card.name}');
        }
        return true; // conferma lo swipe
      },
      onEnd: () {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Nessun altro profilo!')),
        );
      },
      cardBuilder: (context, index, percentX, percentY) =>
          ProfileCardView(card: demoCards[index]),
    )

    Risultato atteso

    Ad ogni swipe compare un log della direzione; a fine stack appare una SnackBar.

  6. 6

    Aggiungere i pulsanti di azione

    Molte app affiancano allo swipe manuale dei pulsanti. Grazie al CardSwiperController possiamo attivare uno swipe programmatico con swipe(direction) e persino annullare l'ultimo con undo().

    Aggiungiamo una Row di pulsanti sotto lo stack.

    Column(
      children: [
        Expanded(
          child: CardSwiper(
            controller: _controller,
            cardsCount: demoCards.length,
            cardBuilder: (context, index, x, y) =>
                ProfileCardView(card: demoCards[index]),
          ),
        ),
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              FloatingActionButton(
                heroTag: 'nope',
                backgroundColor: Colors.red,
                onPressed: () => _controller.swipe(CardSwiperDirection.left),
                child: const Icon(Icons.close),
              ),
              FloatingActionButton(
                heroTag: 'undo',
                backgroundColor: Colors.amber,
                onPressed: () => _controller.undo(),
                child: const Icon(Icons.replay),
              ),
              FloatingActionButton(
                heroTag: 'like',
                backgroundColor: Colors.green,
                onPressed: () => _controller.swipe(CardSwiperDirection.right),
                child: const Icon(Icons.favorite),
              ),
            ],
          ),
        ),
      ],
    )

    Risultato atteso

    Tre pulsanti permettono di rifiutare, annullare l'ultimo swipe e mettere like, con animazione identica allo swipe manuale.

  7. 7

    Rifinire con badge visivi di feedback

    Per un tocco professionale, mostriamo un badge "LIKE" o "NOPE" che appare gradualmente mentre si trascina. Il cardBuilder fornisce horizontalOffsetPercentage (percentX), un valore da -100 a 100 che indica quanto la card è stata trascinata orizzontalmente: lo usiamo per calcolare l'opacità del badge.

    cardBuilder: (context, index, percentX, percentY) {
      final likeOpacity = (percentX / 100).clamp(0.0, 1.0);
      final nopeOpacity = (-percentX / 100).clamp(0.0, 1.0);
      return Stack(
        children: [
          ProfileCardView(card: demoCards[index]),
          Positioned(
            top: 24, left: 24,
            child: Opacity(
              opacity: likeOpacity,
              child: _Badge(text: 'LIKE', color: Colors.green),
            ),
          ),
          Positioned(
            top: 24, right: 24,
            child: Opacity(
              opacity: nopeOpacity,
              child: _Badge(text: 'NOPE', color: Colors.red),
            ),
          ),
        ],
      );
    }
    
    class _Badge extends StatelessWidget {
      final String text;
      final Color color;
      const _Badge({required this.text, required this.color});
    
      @override
      Widget build(BuildContext context) {
        return Container(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
          decoration: BoxDecoration(
            border: Border.all(color: color, width: 3),
            borderRadius: BorderRadius.circular(8),
          ),
          child: Text(text,
            style: TextStyle(color: color, fontSize: 24, fontWeight: FontWeight.bold),
          ),
        );
      }
    }

    Risultato atteso

    Trascinando la card verso destra appare il badge LIKE, verso sinistra il badge NOPE, con opacità proporzionale al trascinamento.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!