DEV Community

Cover image for Creating a Selectable Grid in Flutter: A Step-by-Step Guide
Ramiro - Ramgen
Ramiro - Ramgen

Posted on • Updated on • Originally published at ramagg.com

Creating a Selectable Grid in Flutter: A Step-by-Step Guide

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:

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:

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',
  },
 ];
Enter fullscreen mode Exit fullscreen mode

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.

  int optionSelected = 0;

  void checkOption(int index) {
    setState(() {
      optionSelected = index;
    });
  }
Enter fullscreen mode Exit fullscreen mode

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.

  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,
            )
        ],
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

Now let's take a look at 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;

Enter fullscreen mode Exit fullscreen mode

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:

 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),
                ),
              ),
            ]),
          ),
        ),
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

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 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.

Top comments (1)

Collapse
 
nilan profile image
Nilanchal

@ramgendeploy thnk you for posting this.

I have written a similar article on my blog. But that pulls the data from a REST API and covers more practicle senarios. stacktips.com/articles/how-to-buil...

Demo
https://www.youtube.com/watch?v=bisopNeraJs&list=PLjOeTkAnaMPeX1eIMMtj5m02p_zer9pwP&ab_channel=TheTechMojo