Flutter Layouts & Responsive Design

GridView & Grid Layouts

50 min Lesson 7 of 16

Introduction to GridView

The GridView widget displays items in a two-dimensional grid. It is essentially a scrollable list of items arranged in rows and columns. Flutter provides several constructors optimized for different use cases, from simple fixed-column grids to dynamic layouts that adapt to screen size.

Key Concept: GridView is a scrollable widget that lays out children in a grid pattern. Like ListView, it scrolls in one direction (vertical by default) and supports lazy rendering through builder constructors.

GridView.count

The GridView.count constructor creates a grid with a fixed number of columns (tiles per row). This is the simplest way to create a grid when you know exactly how many columns you want.

GridView.count Example

GridView.count(
  crossAxisCount: 3,  // 3 columns
  mainAxisSpacing: 8.0,   // Vertical space between items
  crossAxisSpacing: 8.0,  // Horizontal space between items
  padding: const EdgeInsets.all(16.0),
  children: List.generate(12, (index) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.blue.shade100,
        borderRadius: BorderRadius.circular(8.0),
      ),
      child: Center(
        child: Text(
          'Item \${index + 1}',
          style: const TextStyle(fontWeight: FontWeight.bold),
        ),
      ),
    );
  }),
)
Warning: Like the default ListView constructor, GridView.count builds all children upfront. For large grids, use GridView.builder instead.

GridView.extent

The GridView.extent constructor creates a grid where each tile has a maximum cross-axis extent. Flutter calculates the number of columns automatically based on the available width and the maximum extent you specify. This makes it naturally responsive.

GridView.extent Example

// Each tile is at most 150px wide
// On a 400px screen: 2 columns (200px each)
// On a 600px screen: 4 columns (150px each)
GridView.extent(
  maxCrossAxisExtent: 150.0,
  mainAxisSpacing: 12.0,
  crossAxisSpacing: 12.0,
  padding: const EdgeInsets.all(16.0),
  children: categories.map((category) {
    return Card(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(category.icon, size: 32.0, color: Colors.blue),
          const SizedBox(height: 8.0),
          Text(
            category.name,
            textAlign: TextAlign.center,
            style: const TextStyle(fontSize: 12.0),
          ),
        ],
      ),
    );
  }).toList(),
)
Tip: Use GridView.extent when you want a responsive grid that automatically adjusts the number of columns based on available space. This is especially useful for grids that should look good on both phones and tablets.

GridView.builder

The GridView.builder constructor creates grid items lazily, only building those that are visible on screen. This is the recommended approach for large or dynamically loaded grids. It requires a SliverGridDelegate to control the grid layout.

GridView.builder with SliverGridDelegateWithFixedCrossAxisCount

// Product catalog grid
GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    mainAxisSpacing: 16.0,
    crossAxisSpacing: 16.0,
    childAspectRatio: 0.75,  // Width / Height ratio
  ),
  padding: const EdgeInsets.all(16.0),
  itemCount: products.length,
  itemBuilder: (context, index) {
    final product = products[index];
    return Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            child: Image.network(
              product.imageUrl,
              width: double.infinity,
              fit: BoxFit.cover,
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  product.name,
                  style: const TextStyle(fontWeight: FontWeight.bold),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
                const SizedBox(height: 4.0),
                Text(
                  '\$\${product.price.toStringAsFixed(2)}',
                  style: TextStyle(
                    color: Colors.green.shade700,
                    fontWeight: FontWeight.w600,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  },
)

SliverGridDelegate Options

The grid delegate controls how items are sized and positioned. Flutter provides two built-in delegates:

Grid Delegate Comparison

// SliverGridDelegateWithFixedCrossAxisCount
// Fixed number of columns - items stretch to fill available space
const SliverGridDelegateWithFixedCrossAxisCount(
  crossAxisCount: 3,          // Exactly 3 columns
  mainAxisSpacing: 8.0,       // Vertical gap
  crossAxisSpacing: 8.0,      // Horizontal gap
  childAspectRatio: 1.0,      // Square tiles (width/height = 1)
)

// SliverGridDelegateWithMaxCrossAxisExtent
// Dynamic columns based on maximum tile width
const SliverGridDelegateWithMaxCrossAxisExtent(
  maxCrossAxisExtent: 200.0,  // Each tile max 200px wide
  mainAxisSpacing: 8.0,
  crossAxisSpacing: 8.0,
  childAspectRatio: 1.5,      // Wider than tall
  // mainAxisExtent: 120.0,   // Alternative: fixed main axis size
)

childAspectRatio Deep Dive

The childAspectRatio property controls the ratio of each tile’s width to its height. Understanding this is crucial for designing good-looking grids.

childAspectRatio Examples

// childAspectRatio = width / height

// Square tiles
childAspectRatio: 1.0     // width == height

// Landscape tiles (wider than tall)
childAspectRatio: 16 / 9  // Widescreen ratio
childAspectRatio: 2.0     // Twice as wide as tall

// Portrait tiles (taller than wide)
childAspectRatio: 0.75    // Product cards
childAspectRatio: 0.5     // Very tall cards
childAspectRatio: 2 / 3   // Classic portrait ratio

// Practical example: responsive product grid
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    childAspectRatio: 0.7,  // Tall cards for product images + info
    mainAxisSpacing: 12.0,
    crossAxisSpacing: 12.0,
  ),
  itemCount: 20,
  itemBuilder: (context, index) => const ProductCard(),
)
Note: If childAspectRatio does not give you enough control, use mainAxisExtent on the delegate to set an exact pixel height for each tile instead.

Practical Example: Photo Gallery Grid

Photo Gallery

class PhotoGalleryScreen extends StatelessWidget {
  final List<String> photoUrls;

  const PhotoGalleryScreen({super.key, required this.photoUrls});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Gallery')),
      body: LayoutBuilder(
        builder: (context, constraints) {
          // Responsive: more columns on wider screens
          final crossAxisCount = constraints.maxWidth > 600 ? 4 : 3;

          return GridView.builder(
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: crossAxisCount,
              mainAxisSpacing: 4.0,
              crossAxisSpacing: 4.0,
            ),
            padding: const EdgeInsets.all(4.0),
            itemCount: photoUrls.length,
            itemBuilder: (context, index) {
              return GestureDetector(
                onTap: () {
                  // Open full-screen photo viewer
                },
                child: Hero(
                  tag: 'photo_\$index',
                  child: Image.network(
                    photoUrls[index],
                    fit: BoxFit.cover,
                    loadingBuilder: (context, child, loadingProgress) {
                      if (loadingProgress == null) return child;
                      return Container(
                        color: Colors.grey.shade200,
                        child: const Center(
                          child: CircularProgressIndicator(),
                        ),
                      );
                    },
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

Practical Example: Dashboard Tiles

Dashboard Grid

class DashboardScreen extends StatelessWidget {
  const DashboardScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final tiles = [
      DashboardTile(
        icon: Icons.shopping_cart,
        title: 'Orders',
        value: '156',
        color: Colors.blue,
      ),
      DashboardTile(
        icon: Icons.people,
        title: 'Users',
        value: '2,340',
        color: Colors.green,
      ),
      DashboardTile(
        icon: Icons.attach_money,
        title: 'Revenue',
        value: '\$12.5K',
        color: Colors.orange,
      ),
      DashboardTile(
        icon: Icons.trending_up,
        title: 'Growth',
        value: '+23%',
        color: Colors.purple,
      ),
    ];

    return GridView.builder(
      gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
        maxCrossAxisExtent: 200.0,
        mainAxisSpacing: 16.0,
        crossAxisSpacing: 16.0,
        childAspectRatio: 1.2,
      ),
      padding: const EdgeInsets.all(16.0),
      itemCount: tiles.length,
      itemBuilder: (context, index) {
        final tile = tiles[index];
        return Card(
          elevation: 2.0,
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(tile.icon, size: 36.0, color: tile.color),
                const SizedBox(height: 8.0),
                Text(
                  tile.value,
                  style: TextStyle(
                    fontSize: 24.0,
                    fontWeight: FontWeight.bold,
                    color: tile.color,
                  ),
                ),
                const SizedBox(height: 4.0),
                Text(
                  tile.title,
                  style: const TextStyle(color: Colors.grey),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}
Tip: Combine GridView.builder with LayoutBuilder to create truly responsive grids. Use the available width from LayoutBuilder constraints to dynamically adjust the number of columns or maximum tile extent.