Creating a Selectable Grid in Flutter: A Step-by-Step Guide
Tags: flutter, beginners, design, mobile
Published: 2021-02-05 20:25:38

In this tutorial, we'll learn how to create a selectable grid in Flutter using the GridView.count widget. We'll also be using a global function to change the current selected item, making this widget useful for a settings page or anywhere you need to select from a grid of items. To demonstrate, we'll be using a list of months as our example.

Before we begin, check out the accompanying video for this post:

{% youtube A-ub3nMuRY4 %}

If you like the content, consider following for more and subscribing to my YouTube channel ramgendeploy 😁

First, let's start with a list of maps for the grid list:

```dart
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',
  },
 ];
```
In the main widget where the grid list is going to be, we have the following in the state:
The `optionSelected` variable is used to select the current item. We then have a function that we set to the children of the grid to select the current id.

```dart 
  int optionSelected = 0;

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

To render the grid view, we use the `GridView.count` widget. This creates a GridView from a list, and we need to specify the number of columns with the `crossAxisCount` property. We then populate the list with a for loop going through the months array, giving the months array a title and image.

The important part here is the `onTap` function. We have a lambda function that runs the `checkOption` function with the corresponding id of the selected item. This id must match the id in the selected flag. If it does, we set the flag to true.


```dart 
  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,
            )
        ],
      ),
    );
  }
```

Now let's take a look at the statelessWidget for the options of the grid:

```dart 
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;

```
We have the title, an img string for the asset image, a VoidCallback for the onTap function, and the selected flag.

In the build method, we have:
```dart
 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),
                ),
              ),
            ]),
          ),
        ),
      ),
    );
  }
```
The tree structure is as follows:

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

Animated containers are great for this kind of thing. In the first one, we change the color and width of the border side. Then in the second, we change the background color of the title.

In some parts of the code, we use the [null-aware operator](https://dart.dev/codelabs/dart-cheatsheet#null-aware-operators) to check if selected is null. If it is, we default to false. If it's not null, we use its value. We also use this in the title in the Text widget because we can't give a null value to the Text widget.

You may be wondering how this performs. Doesn't all the widget get re-rendered when we hit one option? What if we have hundreds of options? I tested this with 200 items and it worked really well. Flutter is great with this kind of stuff. You may have problems if you have a lot going on in the app, but for a settings section, this is an excellent solution.