How to create a Flutter GridView with content-sized items

Source code on GitHub

Whenever you want to lay out items in a grid, the Flutter GridView widget is the first place to go.

GridView offers these properties to decide how items are laid out:

  • crossAxisSpacing
  • mainAxisSpacing
  • crossAxisCount (when using GridView.count or SliverGridDelegateWithFixedCrossAxisCount)
  • maxCrossAxisExtent (when using GridView.extent or SliverGridDelegateWithMaxCrossAxisExtent)
  • childAspectRatio

Depending on what we're after, we can choose how many items we want on the cross axis (with crossAxisCount), or the maximum cross-axis extent (with maxCrossAxisExtent).

In addition to this, childAspectRatio may be used to specify the width / height ratio of all the items in the grid.

Note: childAspectRatio applies the same ratio to all items in the grid, regardless of the size of the content inside them.

This works well for simple layouts.

But it quickly becomes clear that GridView is not flexible enough to represent complex, responsive layouts.

So in this article, we'll explore the limitations of the Flutter GridView widget.

And we'll learn how to build a responsive grid widget with content-sized items using the flutter_layout_grid package, which is based on the CSS Grid Layout spec.

But let's start with GridView and understand its limitations.

What's wrong with GridView?

To illustrate this, let's consider this example:

GridView with two complex items inside it

If the window above has a fixed width, we can tweak all the required properties and get the items to render correctly.

Here's a custom widget that renders the layout above using GridView:

class ItemCardGridView extends StatelessWidget { const ItemCardGridView( {Key? key, required this.crossAxisCount, required this.padding, required this.items}) // we plan to use this with 1 or 2 columns only : assert(crossAxisCount == 1 || crossAxisCount == 2), super(key: key); final int crossAxisCount; final EdgeInsets padding; // list representing the data for all items final List<ItemCardData> items; @override Widget build(BuildContext context) { return GridView.builder( padding: padding, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, mainAxisSpacing: 40, crossAxisSpacing: 24, // width / height: fixed for *all* items childAspectRatio: 0.75, ), // return a custom ItemCard itemBuilder: (context, i) => ItemCard(data: items[i]), itemCount: items.length, ); } }

So far, nothing surprising. We use a GridView.builder to create ItemCard widgets and define all the layout properties with SliverGridDelegateWithFixedCrossAxisCount.

What is complex here is the ItemCard widget, which contains:

  • a banner image
  • a title
  • some metadata
  • some tags
  • a description

Since these values change based on the content, we get some problems if we apply the same childAspectRatio to all grid items.

As proof of this, here's what happens when we resize the window:

Reducing the width causes a bottom overflow on all grid items

And if we try to make our app responsive and switch to a single-column layout when the window gets smaller, this is what we get:

Extra space at the bottom with a single-column layout

Bottom line: using a fixed aspect ratio is a no-go because:

  • if the item width becomes too small, we'll get a bottom overflow
  • if the item width becomes too big, we'll get extra empty space at the bottom

No matter how hard we try, we can't tweak all the GridView properties and make them work for all window sizes.

We need a different layout algorithm

What we really need is a layout algorithm that can work out how tall each item should be as the width changes.

More specifically, the algorithm should calculate the height of the tallest item on each row, and apply that to all items in the row. This means that the size of any given item on the grid depends on the size of other items in the grid.

This is not possible with GridView. And it can't be done with a Stack either.

To accomplish this in Flutter, we need to use a rather esoteric widget called MultiChildRenderObjectWidget.

I'll be honest: things can get quite complex if we try to implement our own custom layout using MultiChildRenderObjectWidget. If you're curious about all the details, you can read this excellent page on StackOverflow:

But that's not what we'll do here. Instead, we'll use a package called flutter_layout_grid that supports content-sized items out-of-the-box.

Content-sized items with the flutter_layout_grid package

The package README describes this as a powerful grid layout system for Flutter, optimized for complex user interface design.

So let's use this!

First of all, we need to add it to our pubspec.yaml:

dependencies: flutter_layout_grid: ^1.0.3

Then, we can define a custom ItemCardLayoutGrid widget to show each ItemCard inside a LayoutGrid widget:

import 'package:flutter_layout_grid/flutter_layout_grid.dart'; class ItemCardLayoutGrid extends StatelessWidget { const ItemCardLayoutGrid({ Key? key, required this.crossAxisCount, required this.items, }) // we only plan to use this with 1 or 2 columns : assert(crossAxisCount == 1 || crossAxisCount == 2), // assume we pass an list of 4 items for simplicity assert(items.length == 4), super(key: key); final int crossAxisCount; final List<ItemCardData> items; @override Widget build(BuildContext context) { return LayoutGrid( // set some flexible track sizes based on the crossAxisCount columnSizes: crossAxisCount == 2 ? [1.fr, 1.fr] : [1.fr], // set all the row sizes to auto (self-sizing height) rowSizes: crossAxisCount == 2 ? const [auto, auto] : const [auto, auto, auto, auto], rowGap: 40, // equivalent to mainAxisSpacing columnGap: 24, // equivalent to crossAxisSpacing // note: there's no childAspectRatio children: [ // render all the cards with *automatic child placement* for (var i = 0; i < items.length; i++) ItemCard(data: items[i]), ], ); } }

Here's how things work.

First of all, we define the columnSizes:

// set some flexible track sizes based on the crossAxisCount columnSizes: crossAxisCount == 2 ? [1.fr, 1.fr] : [1.fr]

This uses a FlexibleTrackSize unit to define how columns should size themselves. 1.fr is the same as setting flex: 1 inside a Flexible widget.


Then, we define the rowSizes:

// set all the row sizes to auto (self-sizing height) // here we assume there will be 4 items in total rowSizes: crossAxisCount == 2 ? const [auto, auto] : const [auto, auto, auto, auto]

In this case, we use auto because we want the rows to size themselves based on the content.

Note: The code above uses the ternary operator to render a 2x2 or 1x4 grid depending on the crossAxisCount, so that we pass a list with the correct number of values to the columnSizes and rowSizes arguments.


After that, we set the rowGap and columnGap properties, which are equivalent to mainAxisSpacing and crossAxisSpacing:

rowGap: 40, // equivalent to mainAxisSpacing columnGap: 24, // equivalent to crossAxisSpacing

And finally, we pass a list of children inside a loop (our ItemCard widgets):

children: [ // render all the cards with *automatic child placement* for (var i = 0; i < items.length; i++) ItemCard(data: items[i]), ]

That's it! If we swap our previous implementation with our new ItemCardLayoutGrid widget, we get content-sized items irrespective of window size:

LayoutGrid with content-sized items over two columns
LayoutGrid with content-sized items over one column

Using LayoutGrid with Slivers

Grid layouts are rarely used on their own and you're likely to show them alongside other scrollable content.

This is what slivers are for and Flutter offers a SliverGrid widget that we can place inside a CustomScrollView (as a replacement for GridView).

However, LayoutGrid doesn't have a "sliver-compatible" replacement.

So if you intend to use it inside a CustomScrollView, make sure to wrap it inside a SliverToBoxAdapter:

child: CustomScrollView( slivers: [ // some slivers SliverToBoxAdapter(child: LayoutGrid(...)), // other slivers ] )

To learn more about slivers, see my video about SliverGrid, SliverToBoxAdapter, and other sliver widgets.

Conclusion

That's it! By replacing GridView with LayoutGrid and setting all the rowSizes to auto, we can get content-sized items that work with a completely responsive layout, without the limitations of a fixed child aspect ratio.

For a complete demo showcasing all the code we have covered, see this project on GitHub:

This shows a complete home page with multiple sections inside a CustomScrollView.

There is much more to flutter_layout_grid than what we have covered here, so make sure to check the documentation to learn about all the other features:

References

While researching this article, I found the following resources useful:

Happy coding!

Want More?

Invest in yourself with my high-quality Flutter courses.

Flutter Foundations Course

Flutter Foundations Course

Learn about State Management, App Architecture, Navigation, Testing, and much more by building a Flutter eCommerce app on iOS, Android, and web.

Flutter & Firebase Masterclass

Flutter & Firebase Masterclass

Learn about Firebase Auth, Cloud Firestore, Cloud Functions, Stripe payments, and much more by building a full-stack eCommerce app with Flutter & Firebase.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Fully updated to Dart 2.15.

Flutter Animations Masterclass

Flutter Animations Masterclass

Master Flutter animations and build a completely custom habit tracking application.