DEV Community

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

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

8 1

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.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

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

Billboard image

📊 A side-by-side product comparison between Sentry and Crashlytics

A free guide pointing out the differences between Sentry and Crashlytics, that’s it. See which is best for your mobile crash reporting needs.

See Comparison

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay