Flutter Selectable Grid

Dec 25, 2020

Let's see how to make a selectable grid in flutter, we are going to use gridview.count and using a global function to change the current selected item, this widget can be useful in a settings page, or well anywhere you need to select from a grid of items, in this example we are going to select months also it has a couple of animations so the user can see that the item is being selected, you can follow this blog or watch this youtube video!

Alright we start with a list of maps for the grid list:

const List<Map<String, dynamic>> months = <Map<String, dynamic>>[
  <String, dynamic>{
    'month': 'January',
    'img': 'assets/images/Jan.jpg',
  },
  <String, dynamic>{
    'month': 'February',
    'img': 'assets/images/Feb.jpg',
  },
  <String, dynamic>{
    'month': 'March',
    'img': 'assets/images/Mar.jpg',
  },
 ];

Then in the main widget where the grid list is going to be we have this in the state

  int optionSelected = 0;

  void checkOption(int index) {
    setState(() {
      optionSelected = index;
    });
  }

optionSelectedis used to select the current item, then we have the function that we are going to set to the children of the grid to select the current id.

Then we have this code in the to render the grid view lets see what we have here.

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Selection and Settings $optionSelected'),
      ),
      body: GridView.count(
        crossAxisCount: 2,
        children: <Widget>[
          for (int i = 0; i < months.length; i++)
            MonthOption(
              months[i]['month'] as String,
              img: months[i]['img'] as String,
              onTap: () => checkOption(i + 1),
              selected: i + 1 == optionSelected,
            )
        ],
      ),
    );
  }

We are using the GridView.count widget this creates a GridView from a list, we need to specify the number of columns with the crossAxisCount then we populate the list with a for loop going through the months array, so we give the months array title, the image.

The important part here is the onTap function we have a lambda function that runs the checkOption with the corresponding id of the selected item, this has to be the same id in the selected flag, so we set the flag to true if the selected id is equal to the corresponding id.

Now lets see the statelessWidget for the options of the grid:

class MonthOption extends StatelessWidget {
  const MonthOption(
    this.title, {
    Key key,
    this.img,
    this.onTap,
    this.selected,
  }) : super(key: key);

  final String title;
  final String img;
  final VoidCallback onTap;
  final bool selected;
  
  ...

So we have the title, and a img string for the asset image, a VoidCallback for the onTap function and the selected flag.

Now in the build we have:

 Widget build(BuildContext context) {
    return Ink.image(
      fit: BoxFit.cover,
      image: AssetImage(img),
      child: InkWell(
        onTap: onTap,
        child: Align(
          alignment: Alignment.bottomCenter,
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 300),
            decoration: BoxDecoration(
              border: Border(
                bottom: BorderSide(
                  color: selected ?? false ? Colors.red : Colors.transparent,
                  width: selected ?? false ? 5 : 0,
                ),
              ),
            ),
            padding: const EdgeInsets.all(8.0),
            child: Row(children: <Widget>[
              AnimatedContainer(
                duration: const Duration(milliseconds: 300),
                padding: const EdgeInsets.all(8),
                decoration: BoxDecoration(
                  color: selected ?? false
                      ? Colors.blue.withOpacity(0.8)
                      : Colors.black54,
                  borderRadius: BorderRadius.circular(10),
                ),
                child: Text(
                  title ?? '',
                  style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      color: Colors.white,
                      fontSize: 16),
                ),
              ),
            ]),
          ),
        ),
      ),
    );
  }

So the tree is like this

Ink.image -> InkWell -> Align -> AnimatedContainer -> Row -> AnimatedContainer

Animated container are awesome for this kind of things in the first one we change, the color and with of the border side, then in the second we change the background color of the title.

Here in some part we use null-aware operator to check if selected is null and if it is we default to false, if its not null we use its value. We also use this in the title in the Text widget because we can't give a null to the Text widget.

Maybe you wonder how this performs because when we it one option doesn't all the widget gets re-rendered what if we have hundreds of options well i tested this with 200 items and it worked really well flutter is really good with this kind of stuff, maybe you'll have problems if you have a lot going on in the app but for a settings section this is awesome.

Ramiro

Hey hi! My name is Ramiro, this is my blog about artificial intelligence, coding and things that are in my head. Follow me on twitter @ramgendeploy so we can chat!