I have a Grid.View.builder with Buttons inside. I want to be able to select just one Button at a time!

At the moment I can select Buttons but when I select other buttons the previous Button stays selected.

How can I achieve this? In my_card_grid.dart or my_card.dart?

This is my_card_grid.dart

import 'package:flutter/material.dart';
import 'package:cards/widgets/my_card.dart';
import 'package:provider/provider.dart';
import 'package:cards/Models/card_data.dart';

class CardsGrid extends StatelessWidget {
  const CardsGrid({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Consumer<MyCardData>(
      builder: (context, cardData, child) {
        return Padding(
          padding: const EdgeInsets.all(10),
          child: GridView.builder(
            clipBehavior: Clip.none,
            itemBuilder: (context, index) {
              final card = cardData.cards[index];
              return MyCard(
                cardTitle: card.name,
                pickerColor: card.cardColor,
                deleteCallback: () {
                  cardData.deleteCallback(card);
                },
              );
            },
            itemCount: cardData.cardCount,
            gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
              maxCrossAxisExtent: 150,
              childAspectRatio: 2.5 / 1,
              crossAxisSpacing: 0,
              mainAxisSpacing: 0,
            ),
          ),
        );
      },
    );
  }
}

and this is my_card.dart

import 'package:cards/Models/card.dart';
import 'package:cards/Screens/add_card.dart';
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:cards/Models/card_data.dart';
import 'package:provider/provider.dart';

class MyCard extends StatefulWidget {
  late String cardTitle;
  Color pickerColor;
  final VoidCallback deleteCallback;

  MyCard(
      {required this.cardTitle,
      required this.pickerColor,
      required this.deleteCallback});

  @override
  State<MyCard> createState() => _MyCardState();
}

class _MyCardState extends State<MyCard> {
  bool selectedCard = false;

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onLongPress: () => showDialog<String>(
        context: context,
        builder: (BuildContext context) => AlertDialog(
          actionsAlignment: MainAxisAlignment.spaceEvenly,
          title: Center(
            child: Text(
              widget.cardTitle,
              style: TextStyle(
                color: widget.pickerColor,
                fontWeight: FontWeight.bold,
                shadows: [
                  Shadow(
                    color: Colors.black.withOpacity(0.3),
                    offset: Offset(0, 0),
                    blurRadius: 15,
                  ),
                ],
              ),
            ),
          ),
          content: const Text(
            'Möchtest du die Karte wirklich löschen?',
            textAlign: TextAlign.center,
            style: TextStyle(
              fontSize: 18,
            ),
          ),
          actions: <Widget>[
            TextButton(
              style: ButtonStyle(
                  shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                      RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(15),
                  )),
                  elevation: MaterialStateProperty.all(10),
                  backgroundColor: MaterialStateProperty.all(Colors.white)),
              onPressed: () {
                widget.deleteCallback();
                Navigator.pop(context);
              },
              child: const Text(
                'Karte löschen',
                style: TextStyle(
                    color: Colors.black,
                    fontWeight: FontWeight.bold,
                    fontSize: 16),
              ),
            ),
            TextButton(
              style: ButtonStyle(
                  shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                    RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(15),
                    ),
                  ),
                  elevation: MaterialStateProperty.all(10),
                  backgroundColor: MaterialStateProperty.all(Colors.white)),
              onPressed: () => Navigator.pop(context),
              child: const Text(
                'Abbrechen',
                style: TextStyle(color: Colors.black, fontSize: 16),
              ),
            ),
          ],
        ),
      ),
      style: ButtonStyle(
          side: MaterialStateProperty.all(BorderSide(
              width: 5, color: selectedCard ? Colors.black : Colors.white)),
          shape: MaterialStateProperty.all(
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
          backgroundColor: MaterialStateProperty.all(widget.pickerColor),
          elevation: MaterialStateProperty.all(10)),
      onPressed: () {
        print(selectedCard);
        setState(() {
          selectedCard = !selectedCard;
        });
      },
      child: FittedBox(
        fit: BoxFit.fitHeight,
        child: Text(
          widget.cardTitle,
          style: TextStyle(
            fontSize: 17,
            color: useWhiteForeground(widget.pickerColor)
                ? const Color(0xffffffff)
                : const Color(0xff000000),
          ),
        ),
      ),
    );
  }
}

Git

Screenshot with expl.


Solution 1: Noir

Just a quick thought, I think we can achieve that by doing following modifications, which basically propagating selection info to parent:

  1. Change MyCard into StatelessWidget, which takes in isSelected for rendering purpose
  2. Modifying CardsGrid into StatefulWidget and adding a state int selectedIdx.
  3. Wrap MyCard component inside GestureDector for adjusting selected Could be something like:
GestureDetector(
  onTap() => setState(() => selected = index),
  child: MyCard(selected == index),
)

As per requested, for Step 2,

class CardsGrid extends StatefulWidget {
  CardsGrid({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _CardsGridState();
}

class _CardsGridState extends State<CardsGrid> {
  int selectedIdx = -1; // or mark as late and initialize in initState() if u prefer

  @override
  Widget build(BuildContext context) {
    return Consumer<MyCardData>(
      builder: (context, cardData, child) {
        return Padding(
          padding: const EdgeInsets.all(10),
          child: GridView.builder(
            clipBehavior: Clip.none,
            itemBuilder: (context, index) {
              final card = cardData.cards[index];
              return GestureDetector( // ADD
                onTap: () => setState(() => selectedIdx = index), // ADD
                child: MyCard(
                  selected: index == selectedIdx, // ADD
                  cardTitle: card.name,
                  pickerColor: card.cardColor,
                  deleteCallback: () {
                    cardData.deleteCallback(card);
                  },
                ), // ADD
              );
            },
            itemCount: cardData.cardCount,
            gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
              maxCrossAxisExtent: 150,
              childAspectRatio: 2.5 / 1,
              crossAxisSpacing: 0,
              mainAxisSpacing: 0,
            ),
          ),
        );
      },
    );
  }
}

Update on 10-Oct-2022: as per requested, here's the sample code, can paste it into your dartpad online to try it out

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: CardsHolderWidget(),
        ),
      ),
    );
  }
}

class CardsHolderWidget extends StatefulWidget {
  const CardsHolderWidget({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _CardsHolderWidgetState();
}

class _CardsHolderWidgetState extends State<CardsHolderWidget> {
  int selected = -1;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemBuilder: (context, index) => GestureDetector(
        onTap: () => setState(() => selected = index),
        child: CardWidget(selected == index, '$index'),
      ),
    );
  }
}

class CardWidget extends StatelessWidget {
  final bool selected;
  final String index;

  const CardWidget(this.selected, this.index, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: selected ? Colors.red : Colors.blue,
      child: Text(index),
    );
  }
}


Solution 2: Randal Schwartz

If you want the logic of a radio button, it should look like a radio button to be familiar. Find out more at https://material.io/components/radio-buttons. There's example Flutter code there for an implementation as well.


Solution 3: Val

So, I've found another article and tried to implement it in mine.

It works!

However, I can not unselect items. Anyone an idea?

here is the code

class MyCard extends StatefulWidget {
  late String cardTitle;
  Color pickerColor;
  final VoidCallback deleteCallback;
  bool isSelected = false;
  Function(int) selectedCard;
  final int index;

  MyCard(
      this.selectedCard, {
        Key? key,
      required this.isSelected,
      required this.cardTitle,
      required this.pickerColor,
      required this.deleteCallback,
      required this.index})
      : super(key: key);

  @override
  State<MyCard> createState() => _MyCardState();
}

class _MyCardState extends State<MyCard> {
  @override
  Widget build(BuildContext context) {
    return TextButton(
      onLongPress: () => showDialog<String>(
        context: context,
        builder: (BuildContext context) => AlertDialog(
          actionsAlignment: MainAxisAlignment.spaceEvenly,
          title: Center(
            child: Text(
              widget.cardTitle,
              style: TextStyle(
                color: widget.pickerColor,
                fontWeight: FontWeight.bold,
                shadows: [
                  Shadow(
                    color: Colors.black.withOpacity(0.3),
                    offset: Offset(0, 0),
                    blurRadius: 15,
                  ),
                ],
              ),
            ),
          ),
          content: const Text(
            'Möchtest du die Karte wirklich löschen?',
            textAlign: TextAlign.center,
            style: TextStyle(
              fontSize: 18,
            ),
          ),
          actions: <Widget>[
            TextButton(
              style: ButtonStyle(
                  shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                      RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(15),
                  )),
                  elevation: MaterialStateProperty.all(10),
                  backgroundColor: MaterialStateProperty.all(Colors.white)),
              onPressed: () {
                widget.deleteCallback();
                Navigator.pop(context);
              },
              child: const Text(
                'Karte löschen',
                style: TextStyle(
                    color: Colors.black,
                    fontWeight: FontWeight.bold,
                    fontSize: 16),
              ),
            ),
            TextButton(
              style: ButtonStyle(
                  shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                    RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(15),
                    ),
                  ),
                  elevation: MaterialStateProperty.all(10),
                  backgroundColor: MaterialStateProperty.all(Colors.white)),
              onPressed: () => Navigator.pop(context),
              child: const Text(
                'Abbrechen',
                style: TextStyle(color: Colors.black, fontSize: 16),
              ),
            ),
          ],
        ),
      ),
      style: ButtonStyle(
          side: MaterialStateProperty.all(BorderSide(
              width: 5, color: widget.isSelected ? Colors.black : Colors.white)),
          shape: MaterialStateProperty.all(
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
          backgroundColor: MaterialStateProperty.all(widget.pickerColor),
          elevation: MaterialStateProperty.all(10)),
      onPressed: () {
        widget.selectedCard(widget.index);
      },
      child: FittedBox(
        fit: BoxFit.fitHeight,
        child: Text(
          widget.cardTitle,
          style: TextStyle(
            fontSize: 17,
            color: useWhiteForeground(widget.pickerColor)
                ? const Color(0xffffffff)
                : const Color(0xff000000),
          ),
        ),
      ),
    );
  }
}

class CardsGrid extends StatefulWidget {
  CardsGrid({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _CardsGridState();
}

class _CardsGridState extends State<CardsGrid> {
  int _selectedCard = -1;
  selectedCard(index) {
    setState(() {
      _selectedCard = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Consumer<MyCardData>(
      builder: (context, cardData, child) {
        return Padding(
          padding: const EdgeInsets.all(10),
          child: GridView.builder(
            clipBehavior: Clip.none,
            itemBuilder: (context, index) {
              final card = cardData.cards[index];
              return MyCard(
                selectedCard,
                index: index,
                isSelected: _selectedCard == index,
                cardTitle: card.name,
                pickerColor: card.cardColor,
                deleteCallback: () {
                  cardData.deleteCallback(card);
                },
              );
            },
            itemCount: cardData.cardCount,
            gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
              maxCrossAxisExtent: 150,
              childAspectRatio: 2.5 / 1,
              crossAxisSpacing: 0,
              mainAxisSpacing: 0,
            ),
          ),
        );
      },
    );
  }
}