Flutter GridView
GridView is a Flutter widget used to display widgets in a two-dimensional, scrollable grid. Instead of displaying items one below another like ListView, GridView arranges items into rows and columns.
GridView is commonly used for product catalogs, photo galleries, dashboards, category menus, course cards, shopping applications, and other interfaces where multiple items need to be displayed in a grid.
Flutter provides several GridView constructors, including GridView, GridView.builder, GridView.count, GridView.extent, and GridView.custom. For large or dynamically generated collections, GridView.builder is particularly useful because children are created on demand. Flutter GridView API Documentation
1. What Is GridView?
A GridView displays widgets in a two-dimensional arrangement and provides scrolling when the content is larger than the available space.
A simple grid can look conceptually like this:
+------------+------------+
| Item 1 | Item 2 |
+------------+------------+
| Item 3 | Item 4 |
+------------+------------+
| Item 5 | Item 6 |
+------------+------------+
Each item is called a grid tile or grid child.
2. Where GridView Is Used
- Product catalogs
- Photo galleries
- Food menus
- Course cards
- Category screens
- Shopping applications
- Social media media grids
- Dashboard cards
- App shortcut menus
- Image galleries
- Game item collections
- Portfolio layouts
3. GridView vs ListView
| Feature | GridView | ListView |
| Layout | Rows and columns | Linear arrangement |
| Typical use | Products, images, categories | Messages, users, settings |
| Scrolling | Usually vertical | Usually vertical |
| Multiple columns | Yes | No |
| Dynamic builder | GridView.builder | ListView.builder |
4. Basic GridView
The basic GridView constructor accepts an explicit list of widgets and requires a gridDelegate.
GridView(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
children: const [
Card(child: Center(child: Text('Item 1'))),
Card(child: Center(child: Text('Item 2'))),
Card(child: Center(child: Text('Item 3'))),
Card(child: Center(child: Text('Item 4'))),
],
)
The gridDelegate controls the layout, size, spacing, and positioning of grid children. GridView gridDelegate Documentation
5. GridView.count
GridView.count is useful when you know the number of columns or rows you want in the cross axis.
GridView.count(
crossAxisCount: 2,
children: const [
Card(child: Center(child: Text('Item 1'))),
Card(child: Center(child: Text('Item 2'))),
Card(child: Center(child: Text('Item 3'))),
Card(child: Center(child: Text('Item 4'))),
],
)
The official Flutter grid recipe demonstrates GridView.count for creating a grid with a fixed number of columns. Flutter Create a Grid List
6. Understanding crossAxisCount
crossAxisCount specifies how many tiles should appear across the cross axis.
GridView.count(
crossAxisCount: 2,
children: [
...
],
)
For a vertical grid:
crossAxisCount: 2 → 2 columns
crossAxisCount: 3 → 3 columns
crossAxisCount: 4 → 4 columns
7. Complete GridView.count Example
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('GridView Example'),
),
body: GridView.count(
padding: const EdgeInsets.all(12),
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: const [
Card(
child: Center(
child: Text('Laptop'),
),
),
Card(
child: Center(
child: Text('Mobile'),
),
),
Card(
child: Center(
child: Text('Tablet'),
),
),
Card(
child: Center(
child: Text('Headphones'),
),
),
],
),
),
);
}
}
8. GridView.builder
GridView.builder is designed for dynamically generated and potentially large collections. It creates grid children on demand instead of requiring all child widgets to be explicitly created up front.
GridView.builder(
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return Card(
child: Center(
child: Text(products[index]),
),
);
},
)
Flutter's API documentation describes GridView.builder as a constructor for a scrollable two-dimensional array whose children can be created on demand. It is appropriate for large or potentially infinite collections. GridView.builder API Documentation
9. Why Use GridView.builder?
- It supports dynamically generated data.
- It is suitable for large collections.
- It creates children on demand.
- It works well with API data.
- It reduces the need to create a complete widget list before rendering.
- It supports product, image, category, and dashboard grids.
10. Basic GridView.builder Syntax
GridView.builder(
itemCount: items.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return YourWidget(
data: items[index],
);
},
)
11. Important GridView.builder Properties
| Property | Purpose |
itemCount | Specifies the number of grid items. |
itemBuilder | Builds an individual grid item. |
gridDelegate | Controls the grid layout. |
crossAxisCount | Controls the number of columns or rows in the cross axis. |
crossAxisSpacing | Adds spacing between columns or cross-axis tiles. |
mainAxisSpacing | Adds spacing between rows or main-axis tiles. |
childAspectRatio | Controls the width-to-height ratio of grid children. |
mainAxisExtent | Specifies the extent of children in the main axis when using the fixed-count delegate. |
padding | Adds space around the grid. |
scrollDirection | Controls the scrolling direction. |
reverse | Reverses the scroll direction. |
controller | Controls and observes scrolling. |
physics | Controls scrolling behavior. |
shrinkWrap | Allows the grid to size itself according to its contents when required. |
12. SliverGridDelegateWithFixedCrossAxisCount
SliverGridDelegateWithFixedCrossAxisCount is commonly used when you want a fixed number of columns.
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
)
The delegate controls the layout of grid tiles, including the number of tiles in the cross axis and their spacing and aspect ratio. SliverGridDelegateWithFixedCrossAxisCount API
13. mainAxisSpacing
mainAxisSpacing controls the spacing between tiles along the main scrolling axis.
GridView.builder(
itemCount: 20,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
),
itemBuilder: (context, index) {
return Card(
child: Center(
child: Text('Item $index'),
),
);
},
)
14. crossAxisSpacing
crossAxisSpacing controls the space between columns in a vertically scrolling grid.
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
)
15. Using Both Spacing Properties
GridView.builder(
itemCount: 20,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
),
itemBuilder: (context, index) {
return Card(
child: Center(
child: Text('Item $index'),
),
);
},
)
16. childAspectRatio
childAspectRatio determines the width-to-height ratio of each grid child.
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1,
)
A value of 1 creates approximately square tiles.
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.5,
)
A larger ratio produces relatively wider tiles, while a smaller ratio produces relatively taller tiles.
17. mainAxisExtent
mainAxisExtent can be used when you want to specify the main-axis extent of each tile directly.
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisExtent: 180,
)
This can be useful when cards need a predictable height.
18. GridView.extent
GridView.extent allows you to specify a maximum cross-axis extent for tiles rather than directly specifying the number of columns.
GridView.extent(
maxCrossAxisExtent: 200,
padding: const EdgeInsets.all(12),
children: const [
Card(child: Center(child: Text('Item 1'))),
Card(child: Center(child: Text('Item 2'))),
Card(child: Center(child: Text('Item 3'))),
Card(child: Center(child: Text('Item 4'))),
],
)
This approach can be useful when you want the number of columns to adapt according to the available width. Flutter's GridView documentation identifies GridView.extent as a constructor that creates tiles with a maximum cross-axis extent. GridView Class Documentation
19. SliverGridDelegateWithMaxCrossAxisExtent
The equivalent delegate-based approach uses SliverGridDelegateWithMaxCrossAxisExtent.
GridView.builder(
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 220,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
),
itemBuilder: (context, index) {
return ProductCard(
product: products[index],
);
},
)
This is useful when the tile width should stay below a maximum value while the number of columns adapts to available space. SliverGridDelegateWithMaxCrossAxisExtent API
20. Dynamic Product Grid
class Product {
final String name;
final double price;
Product({
required this.name,
required this.price,
});
}
final products = [
Product(name: 'Laptop', price: 55000),
Product(name: 'Mobile', price: 30000),
Product(name: 'Tablet', price: 22000),
Product(name: 'Headphones', price: 5000),
Product(name: 'Keyboard', price: 2500),
Product(name: 'Mouse', price: 1200),
];
GridView.builder(
padding: const EdgeInsets.all(12),
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.8,
),
itemBuilder: (context, index) {
final product = products[index];
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.shopping_bag,
size: 50,
),
const SizedBox(height: 10),
Text(
product.name,
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Text('₹${product.price}'),
],
),
),
);
},
)
21. Creating a Category Grid
final categories = [
'Electronics',
'Fashion',
'Shoes',
'Books',
'Furniture',
'Beauty',
'Sports',
'Grocery',
];
GridView.builder(
padding: const EdgeInsets.all(16),
itemCount: categories.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemBuilder: (context, index) {
return Card(
child: Center(
child: Text(
categories[index],
textAlign: TextAlign.center,
),
),
);
},
)
22. GridView with Icons
final menuItems = [
{'title': 'Home', 'icon': Icons.home},
{'title': 'Profile', 'icon': Icons.person},
{'title': 'Orders', 'icon': Icons.shopping_bag},
{'title': 'Settings', 'icon': Icons.settings},
];
GridView.builder(
padding: const EdgeInsets.all(16),
itemCount: menuItems.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemBuilder: (context, index) {
final item = menuItems[index];
return Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
item['icon'] as IconData,
size: 40,
),
const SizedBox(height: 8),
Text(item['title'] as String),
],
),
);
},
)
23. GridView with Images
GridView is frequently used for image galleries.
final imageUrls = [
'https://example.com/image1.jpg',
'https://example.com/image2.jpg',
'https://example.com/image3.jpg',
'https://example.com/image4.jpg',
];
GridView.builder(
padding: const EdgeInsets.all(8),
itemCount: imageUrls.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (context, index) {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
imageUrls[index],
fit: BoxFit.cover,
),
);
},
)
24. GridView with Network Images and Loading
GridView.builder(
itemCount: imageUrls.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (context, index) {
return Image.network(
imageUrls[index],
fit: BoxFit.cover,
loadingBuilder: (
context,
child,
loadingProgress,
) {
if (loadingProgress == null) {
return child;
}
return const Center(
child: CircularProgressIndicator(),
);
},
);
},
)
25. GridView with Card Design
GridView.builder(
padding: const EdgeInsets.all(16),
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 14,
mainAxisSpacing: 14,
childAspectRatio: 0.75,
),
itemBuilder: (context, index) {
final product = products[index];
return Card(
elevation: 3,
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Expanded(
child: Container(
width: double.infinity,
color: Colors.grey.shade200,
child: const Icon(
Icons.image,
size: 60,
),
),
),
Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
Text(
product.name,
textAlign: TextAlign.center,
),
const SizedBox(height: 5),
Text('₹${product.price}'),
],
),
),
],
),
);
},
)
26. GridView with onTap
Grid items can respond to user interaction using InkWell or GestureDetector.
GridView.builder(
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
final product = products[index];
return InkWell(
onTap: () {
print('Selected: ${product.name}');
},
child: Card(
child: Center(
child: Text(product.name),
),
),
);
},
)
27. Navigating from Grid Item to Detail Screen
GridView.builder(
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
final product = products[index];
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ProductDetailsScreen(
product: product,
);
},
),
);
},
child: Card(
child: Center(
child: Text(product.name),
),
),
);
},
)
28. Responsive GridView
A responsive grid adapts the number of columns according to the available screen width.
LayoutBuilder(
builder: (context, constraints) {
int columns;
if (constraints.maxWidth >= 1200) {
columns = 5;
} else if (constraints.maxWidth >= 800) {
columns = 4;
} else if (constraints.maxWidth >= 600) {
columns = 3;
} else {
columns = 2;
}
return GridView.builder(
itemCount: products.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.8,
),
itemBuilder: (context, index) {
return ProductCard(
product: products[index],
);
},
);
},
)
29. Responsive Grid with MaxCrossAxisExtent
Another approach is to allow Flutter to determine the number of columns based on a maximum tile width.
GridView.builder(
padding: const EdgeInsets.all(16),
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 250,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 0.8,
),
itemBuilder: (context, index) {
return ProductCard(
product: products[index],
);
},
)
30. GridView Inside a Column
When a GridView is placed inside a Column, it usually needs a bounded height. Expanded is commonly used when the grid should occupy the remaining space.
Column(
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Products',
style: TextStyle(fontSize: 24),
),
),
Expanded(
child: GridView.builder(
itemCount: products.length,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return ProductCard(
product: products[index],
);
},
),
),
],
)
31. Using shrinkWrap
shrinkWrap: true allows a grid to size itself based on its contents along the scroll direction. It can be useful when the grid is embedded inside another layout, but it may require additional layout work.
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return ProductCard(
product: products[index],
);
},
)
For a large screen containing a single primary grid, avoid using shrinkWrap unnecessarily.
32. Horizontal GridView
GridView can also scroll horizontally. In a horizontal grid, the cross axis becomes vertical, so crossAxisCount represents the number of rows.
GridView.builder(
scrollDirection: Axis.horizontal,
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
),
itemBuilder: (context, index) {
return Card(
child: Center(
child: Text(products[index].name),
),
);
},
)
33. GridView with ScrollController
A ScrollController can be used to monitor and control the grid's scrolling position.
final ScrollController controller = ScrollController();
GridView.builder(
controller: controller,
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return ProductCard(
product: products[index],
);
},
)
Always dispose of a controller created by a stateful widget when it is no longer needed.
@override
void dispose() {
controller.dispose();
super.dispose();
}
34. GridView Reverse Scrolling
GridView.builder(
reverse: true,
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return ProductCard(
product: products[index],
);
},
)
The reverse property reverses the scroll direction.
35. GridView Physics
The physics property controls how scrolling behaves.
GridView.builder(
physics: const BouncingScrollPhysics(),
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return ProductCard(
product: products[index],
);
},
)
To disable scrolling when the grid is nested inside another scrollable:
physics: const NeverScrollableScrollPhysics(),
36. Dynamic Grid with API Data
A common real-world pattern is to fetch products from an API and display them using GridView.builder.
class Product {
final int id;
final String name;
final double price;
Product({
required this.id,
required this.name,
required this.price,
});
factory Product.fromJson(Map json) {
return Product(
id: json['id'],
name: json['name'],
price: (json['price'] as num).toDouble(),
);
}
}
After the API response is converted into a list:
GridView.builder(
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.8,
),
itemBuilder: (context, index) {
final product = products[index];
return Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(product.name),
const SizedBox(height: 8),
Text('₹${product.price}'),
],
),
);
},
)
37. Loading, Error, Empty, and Success States
A production grid should handle different data states.
if (isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (hasError) {
return const Center(
child: Text('Failed to load products'),
);
}
if (products.isEmpty) {
return const Center(
child: Text('No products found'),
);
}
return GridView.builder(
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return ProductCard(
product: products[index],
);
},
);
38. Adding Items Dynamically
A GridView can automatically display new items after the underlying list changes and the state is rebuilt.
final List items = [
'Item 1',
'Item 2',
'Item 3',
];
void addItem() {
setState(() {
items.add(
'Item ${items.length + 1}',
);
});
}
Display the updated collection:
GridView.builder(
itemCount: items.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return Card(
child: Center(
child: Text(items[index]),
),
);
},
)
39. Removing Items Dynamically
void removeItem(int index) {
setState(() {
items.removeAt(index);
});
}
Use the method from a grid item:
GridView.builder(
itemCount: items.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return Card(
child: Stack(
children: [
Center(
child: Text(items[index]),
),
Positioned(
top: 4,
right: 4,
child: IconButton(
onPressed: () {
removeItem(index);
},
icon: const Icon(Icons.delete),
),
),
],
),
);
},
)
40. Using Keys in Dynamic Grids
Stable keys can help Flutter preserve widget identity when items are inserted, removed, or reordered.
GridView.builder(
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
final product = products[index];
return ProductCard(
key: ValueKey(product.id),
product: product,
);
},
)
If the order of children can change, findChildIndexCallback can also be relevant for preserving state mapping. GridView.builder API Documentation
41. GridView with Selection
class SelectionGrid extends StatefulWidget {
const SelectionGrid({super.key});
@override
State createState() => _SelectionGridState();
}
class _SelectionGridState extends State {
final List items = [
'Flutter',
'Dart',
'Firebase',
'Android',
];
int? selectedIndex;
@override
Widget build(BuildContext context) {
return GridView.builder(
padding: const EdgeInsets.all(16),
itemCount: items.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemBuilder: (context, index) {
final selected = selectedIndex == index;
return GestureDetector(
onTap: () {
setState(() {
selectedIndex = index;
});
},
child: Card(
child: Center(
child: Text(
items[index],
style: TextStyle(
fontWeight: selected
? FontWeight.bold
: FontWeight.normal,
),
),
),
),
);
},
);
}
}
42. GridView with Checkbox
final List selectedItems = [
false,
false,
false,
false,
];
GridView.builder(
itemCount: items.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
return Card(
child: CheckboxListTile(
title: Text(items[index]),
value: selectedItems[index],
onChanged: (value) {
setState(() {
selectedItems[index] = value ?? false;
});
},
),
);
},
)
43. GridView and FutureBuilder
For asynchronous data, FutureBuilder can be combined with GridView.builder.
Future> fetchProducts() async {
await Future.delayed(
const Duration(seconds: 2),
);
return [
'Laptop',
'Phone',
'Tablet',
'Keyboard',
'Mouse',
'Headphones',
];
}
FutureBuilder>(
future: fetchProducts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return const Center(
child: Text('Something went wrong'),
);
}
final items = snapshot.data ?? [];
if (items.isEmpty) {
return const Center(
child: Text('No items available'),
);
}
return GridView.builder(
itemCount: items.length,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemBuilder: (context, index) {
return Card(
child: Center(
child: Text(items[index]),
),
);
},
);
},
)
44. Pagination with GridView
When a server contains thousands of products, loading everything at once may be unnecessary. Pagination loads data in smaller pages.
- Load the first page.
- Display the first set of products.
- Monitor the scroll position.
- When the user approaches the bottom, request another page.
- Append the new products to the existing collection.
- Continue until the server reports that there are no more items.
final ScrollController controller = ScrollController();
@override
void initState() {
super.initState();
controller.addListener(() {
if (controller.position.pixels >=
controller.position.maxScrollExtent - 300) {
loadMoreProducts();
}
});
}
Future loadMoreProducts() async {
// Request the next page from the API.
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
45. Avoid API Calls Directly Inside build()
Avoid starting a new network request directly inside build(), because the build method can execute multiple times.
Instead, load data in an appropriate lifecycle method or through your chosen state-management/data-fetching architecture.
@override
void initState() {
super.initState();
loadProducts();
}
Future loadProducts() async {
// Fetch products.
}
46. GridView Performance
GridView.builder uses a builder delegate so grid children can be created on demand. This makes it suitable for large collections. GridView.builder Documentation
Performance Best Practices
- Use
GridView.builder for large or dynamic grids.
- Provide
itemCount whenever the item count is known.
- Keep grid item widgets reasonably simple.
- Avoid unnecessary rebuilds.
- Use stable keys for stateful dynamic items when identity matters.
- Avoid unnecessary
shrinkWrap: true.
- Avoid unnecessary nested scrollable widgets.
- Use appropriate image sizes and caching strategies for image-heavy grids.
- Use pagination for very large remote datasets.
- Use an appropriate
gridDelegate for the required layout.
47. GridView with Reusable ProductCard
Reusable components make large applications easier to maintain.
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({
super.key,
required this.product,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 3,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.shopping_bag,
size: 50,
),
const SizedBox(height: 10),
Text(
product.name,
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Text('₹${product.price}'),
],
),
),
);
}
}
Then use the reusable card inside the grid:
GridView.builder(
itemCount: products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.8,
),
itemBuilder: (context, index) {
return ProductCard(
product: products[index],
);
},
)
48. Complete Responsive Product Grid Example
import 'package:flutter/material.dart';
class Product {
final int id;
final String name;
final double price;
Product({
required this.id,
required this.name,
required this.price,
});
}
class ProductGridScreen extends StatelessWidget {
ProductGridScreen({super.key});
final List products = [
Product(id: 1, name: 'Laptop', price: 55000),
Product(id: 2, name: 'Mobile', price: 30000),
Product(id: 3, name: 'Tablet', price: 22000),
Product(id: 4, name: 'Keyboard', price: 2500),
Product(id: 5, name: 'Mouse', price: 1200),
Product(id: 6, name: 'Headphones', price: 5000),
Product(id: 7, name: 'Monitor', price: 18000),
Product(id: 8, name: 'Smart Watch', price: 8000),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
),
body: GridView.builder(
padding: const EdgeInsets.all(16),
itemCount: products.length,
gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 250,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 0.8,
),
itemBuilder: (context, index) {
final product = products[index];
return Card(
elevation: 3,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.shopping_bag,
size: 55,
),
const SizedBox(height: 12),
Text(
product.name,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'₹${product.price}',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
),
);
}
}
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: ProductGridScreen(),
),
);
}
49. GridView Constructors
| Constructor | Purpose |
GridView() | Creates a grid with an explicitly supplied list of children and a custom grid delegate. |
GridView.builder() | Creates grid children on demand and is suitable for large or dynamic collections. |
GridView.count() | Creates a grid with a fixed number of tiles in the cross axis. |
GridView.extent() | Creates a grid where tiles have a maximum cross-axis extent. |
GridView.custom() | Provides custom grid and child delegates. |
Flutter's current API documents these GridView constructors and their purposes. GridView Class API
50. GridView vs GridView.builder
| GridView | GridView.builder |
| Uses an explicit list of widgets. | Creates widgets through a builder. |
| Convenient for small collections. | Suitable for large or dynamic collections. |
| All supplied child widgets are part of the explicit child list. | Children are created on demand. |
| Simple for fixed content. | Useful for API and database data. |
51. GridView vs GridView.count vs GridView.extent
| Widget | Main Idea |
GridView | Custom grid delegate with explicit children. |
GridView.count | Specify the number of cross-axis tiles. |
GridView.extent | Specify maximum cross-axis tile extent. |
GridView.builder | Build dynamic children on demand. |
52. GridView and CustomScrollView
A GridView is built on top of Flutter's sliver-based scrolling system. When an application needs a more complex scrolling layout, a GridView can be combined conceptually with other slivers through CustomScrollView.
For example, a complex page may contain:
- A
SliverAppBar
- A
SliverList
- A
SliverGrid
- Additional slivers
CustomScrollView(
slivers: [
const SliverAppBar(
title: Text('Products'),
floating: true,
),
SliverGrid.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: products.length,
itemBuilder: (context, index) {
return ProductCard(
product: products[index],
);
},
),
],
)
Flutter documents GridView as essentially a CustomScrollView containing a single SliverGrid. This makes it possible to migrate to CustomScrollView when a page requires multiple sliver-based sections. GridView and CustomScrollView Documentation
53. Common GridView Mistakes
Mistake 1: Forgetting gridDelegate
GridView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return Text(products[index].name);
},
)
GridView.builder requires a gridDelegate.
Mistake 2: Using the Wrong crossAxisCount
If cards become too narrow, reduce the number of columns or use SliverGridDelegateWithMaxCrossAxisExtent.
Mistake 3: Incorrect itemCount
GridView.builder(
itemCount: 20,
itemBuilder: ...
)
If the data collection contains fewer than 20 items, indexing it directly may produce an error. When displaying the complete collection, use the actual collection length.
Mistake 4: Unnecessary shrinkWrap
Using shrinkWrap: true everywhere can add unnecessary layout work. Use it when the surrounding layout actually requires it.
Mistake 5: Large Grid with Explicit Children
For large dynamically generated data, prefer GridView.builder instead of constructing a huge explicit widget list.
54. Best Practices
- Use
GridView.builder for large and dynamic grids.
- Provide
itemCount whenever the number of items is known.
- Use
SliverGridDelegateWithFixedCrossAxisCount when you want a specific number of columns.
- Use
SliverGridDelegateWithMaxCrossAxisExtent when you want adaptive tile sizing.
- Use
crossAxisSpacing and mainAxisSpacing for consistent spacing.
- Use
childAspectRatio or mainAxisExtent to control card dimensions.
- Use reusable widgets such as
ProductCard for maintainable code.
- Use stable keys when dynamic item identity matters.
- Use pagination for very large API collections.
- Handle loading, error, and empty states.
- Avoid unnecessary nested scroll views.
- Use
Expanded when a grid needs to fill available space inside a Column.
- Use
shrinkWrap carefully.
- Optimize images in image-heavy grids.
55. Practice Exercises
- Create a 2-column product grid.
- Create a 3-column category grid.
- Create a photo gallery using
GridView.builder.
- Create a responsive product grid using
SliverGridDelegateWithMaxCrossAxisExtent.
- Add product names and prices to every grid card.
- Add an Add Product button that dynamically inserts products.
- Add a Delete button to every product card.
- Implement product selection using
GestureDetector or InkWell.
- Navigate from a grid card to a product details page.
- Create a grid that loads data from an API.
- Add loading, error, and empty states.
- Implement pagination for a large product catalog.
- Create a horizontal GridView containing two rows.
- Build a dashboard using GridView cards.
56. Quick Revision
| Concept | Key Point |
| GridView | Displays widgets in a scrollable two-dimensional grid. |
| GridView.builder | Builds grid children on demand. |
| GridView.count | Creates a grid with a fixed cross-axis count. |
| GridView.extent | Creates a grid using maximum cross-axis extent. |
| gridDelegate | Controls grid tile layout. |
| crossAxisCount | Controls the number of columns in a vertical grid. |
| mainAxisSpacing | Controls spacing along the main axis. |
| crossAxisSpacing | Controls spacing along the cross axis. |
| childAspectRatio | Controls width-to-height ratio. |
| mainAxisExtent | Controls child extent along the main axis. |
| itemCount | Specifies the number of children. |
| itemBuilder | Creates each grid item. |
| shrinkWrap | Sizes the grid to its contents when required. |
| ScrollController | Controls and monitors grid scrolling. |
57. Official Flutter Resources
58. Learn Flutter with JustAcademy
For structured Flutter training, practical learning, and course guidance, visit the following resources: