[{"data":1,"prerenderedAt":76},["ShallowReactive",2],{"tutorial-creare-uno-swipe-di-card-in-stile-tinder-in-flutter-con-flutter-card-swiper":3,"comments-tutorial-creare-uno-swipe-di-card-in-stile-tinder-in-flutter-con-flutter-card-swiper":75},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"video_url":10,"difficulty":11,"estimated_minutes":12,"flutter_version":13,"status":14,"published_at":15,"meta_title":16,"meta_description":17,"category":18,"author":22,"steps":24},48,"Creare uno swipe di card in stile Tinder in Flutter con flutter_card_swiper","creare-uno-swipe-di-card-in-stile-tinder-in-flutter-con-flutter-card-swiper","Impara a realizzare uno stack di card interattive con swipe direzionale in stile Tinder usando il pacchetto flutter_card_swiper, gestendo eventi e azioni programmatiche.","## Card swipe in stile Tinder\n\nLe 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.\n\nIn questo tutorial useremo il pacchetto [`flutter_card_swiper`](https:\u002F\u002Fpub.dev\u002Fpackages\u002Fflutter_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\u002Fbasso.\n\nAlla fine avremo un widget riutilizzabile con pulsanti di azione, callback sugli eventi di swipe e gestione della fine dello stack.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F7cc197c6-b00c-4eb9-b82a-e9d34093e377.jpg",null,"intermediate",35,"3.x","published","2026-07-25T04:30:57+00:00","Card swipe Tinder in Flutter con flutter_card_swiper","Guida pratica per creare uno stack di card con swipe direzionale in stile Tinder in Flutter usando flutter_card_swiper: eventi, azioni e reset.",{"id":19,"name":20,"slug":21},1,"Guide","guide",{"id":19,"name":23},"Flutter Bot",[25,32,40,47,54,61,68],{"id":26,"position":19,"title":27,"body":28,"code_snippet":29,"code_language":30,"expected_result":31,"demo_url":10,"video_url":10},323,"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.\n\n```bash\nflutter pub add flutter_card_swiper\n```\n\nQuesto comando aggiunge automaticamente la voce corretta in `pubspec.yaml` e scarica il pacchetto.","dependencies:\n  flutter:\n    sdk: flutter\n  flutter_card_swiper: ^7.0.0","yaml","Il pacchetto viene scaricato senza errori e risulta disponibile per l'import.",{"id":33,"position":34,"title":35,"body":36,"code_snippet":37,"code_language":38,"expected_result":39,"demo_url":10,"video_url":10},324,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 {\n  final String name;\n  final int age;\n  final String imageUrl;\n  final Color color;\n\n  const ProfileCard({\n    required this.name,\n    required this.age,\n    required this.imageUrl,\n    required this.color,\n  });\n}\n\nfinal demoCards = \u003CProfileCard>[\n  const ProfileCard(name: 'Alice', age: 27, imageUrl: 'https:\u002F\u002Fpicsum.photos\u002Fid\u002F64\u002F400\u002F600', color: Colors.pinkAccent),\n  const ProfileCard(name: 'Marco', age: 31, imageUrl: 'https:\u002F\u002Fpicsum.photos\u002Fid\u002F91\u002F400\u002F600', color: Colors.blueAccent),\n  const ProfileCard(name: 'Giulia', age: 24, imageUrl: 'https:\u002F\u002Fpicsum.photos\u002Fid\u002F823\u002F400\u002F600', color: Colors.deepPurpleAccent),\n  const ProfileCard(name: 'Luca', age: 29, imageUrl: 'https:\u002F\u002Fpicsum.photos\u002Fid\u002F1005\u002F400\u002F600', color: Colors.tealAccent),\n];","dart","Hai una lista di 4 card pronte da mostrare nello stack.",{"id":41,"position":42,"title":43,"body":44,"code_snippet":45,"code_language":38,"expected_result":46,"demo_url":10,"video_url":10},325,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 {\n  final ProfileCard card;\n  const ProfileCardView({super.key, required this.card});\n\n  @override\n  Widget build(BuildContext context) {\n    return ClipRRect(\n      borderRadius: BorderRadius.circular(24),\n      child: Stack(\n        fit: StackFit.expand,\n        children: [\n          Image.network(card.imageUrl, fit: BoxFit.cover,\n            errorBuilder: (_, __, ___) => Container(color: card.color)),\n          const DecoratedBox(\n            decoration: BoxDecoration(\n              gradient: LinearGradient(\n                begin: Alignment.center,\n                end: Alignment.bottomCenter,\n                colors: [Colors.transparent, Colors.black87],\n              ),\n            ),\n          ),\n          Positioned(\n            left: 20,\n            bottom: 24,\n            child: Text('${card.name}, ${card.age}',\n              style: const TextStyle(\n                color: Colors.white,\n                fontSize: 26,\n                fontWeight: FontWeight.bold,\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}","Una card con immagine, sfumatura e nome\u002Fetà sovrapposti in basso a sinistra.",{"id":48,"position":49,"title":50,"body":51,"code_snippet":52,"code_language":38,"expected_result":53,"demo_url":10,"video_url":10},326,4,"Inserire il CardSwiper nella schermata","Ora usiamo il widget `CardSwiper`. Ha bisogno di:\n\n- `cardsCount`: il numero di card\n- `cardBuilder`: una funzione che costruisce la card all'indice richiesto\n- un `CardSwiperController` per controllarlo programmaticamente (utile per i pulsanti nel passo successivo)\n\nRicorda di dichiarare il controller in uno `StatefulWidget` e di rilasciarlo nel `dispose`.","class SwipeScreen extends StatefulWidget {\n  const SwipeScreen({super.key});\n\n  @override\n  State\u003CSwipeScreen> createState() => _SwipeScreenState();\n}\n\nclass _SwipeScreenState extends State\u003CSwipeScreen> {\n  final CardSwiperController _controller = CardSwiperController();\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Scopri profili')),\n      body: Padding(\n        padding: const EdgeInsets.all(16),\n        child: CardSwiper(\n          controller: _controller,\n          cardsCount: demoCards.length,\n          numberOfCardsDisplayed: 3,\n          backCardOffset: const Offset(0, 40),\n          padding: const EdgeInsets.all(24),\n          cardBuilder: (context, index, percentX, percentY) {\n            return ProfileCardView(card: demoCards[index]);\n          },\n        ),\n      ),\n    );\n  }\n}","Vedi uno stack di card trascinabili con effetto di profondità sulle card retrostanti.",{"id":55,"position":56,"title":57,"body":58,"code_snippet":59,"code_language":38,"expected_result":60,"demo_url":10,"video_url":10},327,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).\n\nUsiamo anche `onEnd` per sapere quando lo stack è terminato.","CardSwiper(\n  controller: _controller,\n  cardsCount: demoCards.length,\n  numberOfCardsDisplayed: 3,\n  onSwipe: (previousIndex, currentIndex, direction) {\n    final card = demoCards[previousIndex];\n    switch (direction) {\n      case CardSwiperDirection.right:\n        debugPrint('LIKE a ${card.name}');\n        break;\n      case CardSwiperDirection.left:\n        debugPrint('NOPE a ${card.name}');\n        break;\n      default:\n        debugPrint('Swipe ${direction.name} su ${card.name}');\n    }\n    return true; \u002F\u002F conferma lo swipe\n  },\n  onEnd: () {\n    ScaffoldMessenger.of(context).showSnackBar(\n      const SnackBar(content: Text('Nessun altro profilo!')),\n    );\n  },\n  cardBuilder: (context, index, percentX, percentY) =>\n      ProfileCardView(card: demoCards[index]),\n)","Ad ogni swipe compare un log della direzione; a fine stack appare una SnackBar.",{"id":62,"position":63,"title":64,"body":65,"code_snippet":66,"code_language":38,"expected_result":67,"demo_url":10,"video_url":10},328,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()`.\n\nAggiungiamo una `Row` di pulsanti sotto lo stack.","Column(\n  children: [\n    Expanded(\n      child: CardSwiper(\n        controller: _controller,\n        cardsCount: demoCards.length,\n        cardBuilder: (context, index, x, y) =>\n            ProfileCardView(card: demoCards[index]),\n      ),\n    ),\n    Padding(\n      padding: const EdgeInsets.symmetric(vertical: 16),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n        children: [\n          FloatingActionButton(\n            heroTag: 'nope',\n            backgroundColor: Colors.red,\n            onPressed: () => _controller.swipe(CardSwiperDirection.left),\n            child: const Icon(Icons.close),\n          ),\n          FloatingActionButton(\n            heroTag: 'undo',\n            backgroundColor: Colors.amber,\n            onPressed: () => _controller.undo(),\n            child: const Icon(Icons.replay),\n          ),\n          FloatingActionButton(\n            heroTag: 'like',\n            backgroundColor: Colors.green,\n            onPressed: () => _controller.swipe(CardSwiperDirection.right),\n            child: const Icon(Icons.favorite),\n          ),\n        ],\n      ),\n    ),\n  ],\n)","Tre pulsanti permettono di rifiutare, annullare l'ultimo swipe e mettere like, con animazione identica allo swipe manuale.",{"id":69,"position":70,"title":71,"body":72,"code_snippet":73,"code_language":38,"expected_result":74,"demo_url":10,"video_url":10},329,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) {\n  final likeOpacity = (percentX \u002F 100).clamp(0.0, 1.0);\n  final nopeOpacity = (-percentX \u002F 100).clamp(0.0, 1.0);\n  return Stack(\n    children: [\n      ProfileCardView(card: demoCards[index]),\n      Positioned(\n        top: 24, left: 24,\n        child: Opacity(\n          opacity: likeOpacity,\n          child: _Badge(text: 'LIKE', color: Colors.green),\n        ),\n      ),\n      Positioned(\n        top: 24, right: 24,\n        child: Opacity(\n          opacity: nopeOpacity,\n          child: _Badge(text: 'NOPE', color: Colors.red),\n        ),\n      ),\n    ],\n  );\n}\n\nclass _Badge extends StatelessWidget {\n  final String text;\n  final Color color;\n  const _Badge({required this.text, required this.color});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),\n      decoration: BoxDecoration(\n        border: Border.all(color: color, width: 3),\n        borderRadius: BorderRadius.circular(8),\n      ),\n      child: Text(text,\n        style: TextStyle(color: color, fontSize: 24, fontWeight: FontWeight.bold),\n      ),\n    );\n  }\n}","Trascinando la card verso destra appare il badge LIKE, verso sinistra il badge NOPE, con opacità proporzionale al trascinamento.",[],1785219597819]