responsive_gridview 2.0.0
responsive_gridview: ^2.0.0 copied to clipboard
A flexible and responsive GridView for Flutter that adapts to screen sizes, breakpoints, custom column counts, and auto-fit item widths.
import 'package:flutter/material.dart';
import 'package:responsive_gridview/responsive_gridview.dart';
/// Main entry point for the Responsive GridView example application.
void main() {
runApp(const ResponsiveGridViewApp());
}
/// The root widget of the example application.
class ResponsiveGridViewApp extends StatelessWidget {
/// Creates the root application widget.
const ResponsiveGridViewApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Responsive GridView Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6366F1),
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6366F1),
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system,
home: const ExampleHomePage(),
);
}
}
/// The home page showcasing multiple ResponsiveGridView use cases and interactive playgrounds.
class ExampleHomePage extends StatefulWidget {
/// Creates the home page widget.
const ExampleHomePage({super.key});
@override
State<ExampleHomePage> createState() => _ExampleHomePageState();
}
class _ExampleHomePageState extends State<ExampleHomePage> {
int _selectedTabIndex = 0;
// Interactive playground state
int _columns = 3;
double _horizontalSpacing = 16.0;
double _verticalSpacing = 16.0;
double _padding = 16.0;
int _itemCount = 8;
double? _aspectRatio = 1.2;
// Auto-fit state
double _minItemWidth = 180.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Responsive GridView'),
elevation: 1,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: [
_buildTabButton(0, 'Playground', Icons.tune),
_buildTabButton(1, 'Breakpoints', Icons.devices),
_buildTabButton(2, 'Auto-Fit Width', Icons.auto_awesome),
_buildTabButton(3, 'Builder Pattern', Icons.list_alt),
],
),
),
),
),
body: IndexedStack(
index: _selectedTabIndex,
children: [
_buildPlaygroundTab(),
_buildBreakpointsTab(),
_buildAutoFitTab(),
_buildBuilderTab(),
],
),
);
}
Widget _buildTabButton(int index, String label, IconData icon) {
final isSelected = _selectedTabIndex == index;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
child: FilterChip(
selected: isSelected,
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18),
const SizedBox(width: 6),
Text(label),
],
),
onSelected: (_) => setState(() => _selectedTabIndex = index),
),
);
}
// --- TAB 1: INTERACTIVE PLAYGROUND ---
Widget _buildPlaygroundTab() {
return Column(
children: [
_buildPlaygroundControls(),
const Divider(height: 1),
Expanded(
child: SingleChildScrollView(
child: ResponsiveGridView(
column: _columns,
horizontalSpacing: _horizontalSpacing,
verticalSpacing: _verticalSpacing,
padding: EdgeInsets.all(_padding),
childAspectRatio: _aspectRatio,
children: List.generate(
_itemCount,
(index) => _buildGridCard(
title: 'Item #${index + 1}',
subtitle:
'Col $_columns • Spacing ${_horizontalSpacing.toInt()}',
index: index,
),
),
),
),
),
],
);
}
Widget _buildPlaygroundControls() {
return ExpansionTile(
initiallyExpanded: true,
leading: const Icon(Icons.settings),
title: Text(
'Controls: Columns: $_columns | Spacing: ${_horizontalSpacing.toInt()}px | Items: $_itemCount',
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
children: [
Row(
children: [
Expanded(
child: _buildSlider(
label: 'Columns: $_columns',
value: _columns.toDouble(),
min: 1,
max: 6,
divisions: 5,
onChanged: (val) =>
setState(() => _columns = val.round()),
),
),
const SizedBox(width: 16),
Expanded(
child: _buildSlider(
label: 'Items: $_itemCount',
value: _itemCount.toDouble(),
min: 1,
max: 24,
divisions: 23,
onChanged: (val) =>
setState(() => _itemCount = val.round()),
),
),
],
),
Row(
children: [
Expanded(
child: _buildSlider(
label: 'Horiz Spacing: ${_horizontalSpacing.toInt()}px',
value: _horizontalSpacing,
min: 0,
max: 32,
divisions: 8,
onChanged: (val) =>
setState(() => _horizontalSpacing = val),
),
),
const SizedBox(width: 16),
Expanded(
child: _buildSlider(
label: 'Vert Spacing: ${_verticalSpacing.toInt()}px',
value: _verticalSpacing,
min: 0,
max: 32,
divisions: 8,
onChanged: (val) =>
setState(() => _verticalSpacing = val),
),
),
],
),
Row(
children: [
Expanded(
child: _buildSlider(
label: 'Padding: ${_padding.toInt()}px',
value: _padding,
min: 0,
max: 32,
divisions: 8,
onChanged: (val) => setState(() => _padding = val),
),
),
const SizedBox(width: 16),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Aspect Ratio:',
style: TextStyle(fontSize: 12)),
DropdownButton<double?>(
value: _aspectRatio,
isDense: true,
items: const [
DropdownMenuItem(
value: null, child: Text('Auto/Content')),
DropdownMenuItem(
value: 1.0, child: Text('1:1 Square')),
DropdownMenuItem(
value: 1.2, child: Text('1.2 Rect')),
DropdownMenuItem(
value: 16 / 9, child: Text('16:9 Wide')),
],
onChanged: (val) =>
setState(() => _aspectRatio = val),
),
],
),
),
],
),
],
),
),
],
);
}
Widget _buildSlider({
required String label,
required double value,
required double min,
required double max,
required int divisions,
required ValueChanged<double> onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 12)),
Slider(
value: value,
min: min,
max: max,
divisions: divisions,
onChanged: onChanged,
),
],
);
}
// --- TAB 2: RESPONSIVE BREAKPOINTS ---
Widget _buildBreakpointsTab() {
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
String activeBp;
int activeCols;
if (width >= 1536) {
activeBp = 'XL (>= 1536px)';
activeCols = 6;
} else if (width >= 1200) {
activeBp = 'LG (>= 1200px)';
activeCols = 4;
} else if (width >= 900) {
activeBp = 'MD (>= 900px)';
activeCols = 3;
} else if (width >= 600) {
activeBp = 'SM (>= 600px)';
activeCols = 2;
} else {
activeBp = 'XS (< 600px)';
activeCols = 1;
}
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
margin: const EdgeInsets.all(16),
color: Theme.of(context).colorScheme.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text(
'Current Width: ${width.toStringAsFixed(1)}px',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
'Active Breakpoint: $activeBp ➔ $activeCols Columns',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Resize your browser window or rotate your device to see columns automatically adapt!',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12),
),
],
),
),
),
ResponsiveGridView(
columns: const ResponsiveColumns(
xs: 1,
sm: 2,
md: 3,
lg: 4,
xl: 6,
),
horizontalSpacing: 16,
verticalSpacing: 16,
padding: const EdgeInsets.symmetric(horizontal: 16),
childAspectRatio: 1.3,
children: List.generate(
12,
(index) => _buildGridCard(
title: 'Tile #${index + 1}',
subtitle: 'Breakpoint Layout',
index: index,
),
),
),
const SizedBox(height: 24),
],
),
);
},
);
}
// --- TAB 3: AUTO-FIT MIN ITEM WIDTH ---
Widget _buildAutoFitTab() {
return Column(
children: [
Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Auto-Fit Minimum Item Width:',
style: TextStyle(fontWeight: FontWeight.bold)),
Text('${_minItemWidth.toInt()} px',
style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
Slider(
value: _minItemWidth,
min: 100,
max: 400,
divisions: 30,
onChanged: (val) => setState(() => _minItemWidth = val),
),
const Text(
'The grid automatically calculates how many items fit based on available container width.',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
),
Expanded(
child: SingleChildScrollView(
child: ResponsiveGridView(
minItemWidth: _minItemWidth,
horizontalSpacing: 16,
verticalSpacing: 16,
padding: const EdgeInsets.symmetric(horizontal: 16),
childAspectRatio: 1.2,
children: List.generate(
12,
(index) => _buildGridCard(
title: 'Product #${index + 1}',
subtitle: 'Min width ${_minItemWidth.toInt()}px',
index: index,
),
),
),
),
),
],
);
}
// --- TAB 4: BUILDER PATTERN ---
Widget _buildBuilderTab() {
return ResponsiveGridView.builder(
minItemWidth: 200,
horizontalSpacing: 16,
verticalSpacing: 16,
padding: const EdgeInsets.all(16),
itemCount: 40,
childAspectRatio: 1.4,
itemBuilder: (context, index) => _buildGridCard(
title: 'Lazy Item #$index',
subtitle: 'Indexed item from builder',
index: index,
),
);
}
Widget _buildGridCard({
required String title,
required String subtitle,
required int index,
}) {
final colors = [
Colors.indigo,
Colors.teal,
Colors.deepOrange,
Colors.purple,
Colors.blue,
Colors.pink,
Colors.amber.shade900,
Colors.cyan.shade800,
];
final color = colors[index % colors.length];
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
color.withAlpha(40),
color.withAlpha(15),
],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: 16,
backgroundColor: color,
child: Text(
'${index + 1}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
const Spacer(),
Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
fontSize: 11,
color: Theme.of(context).textTheme.bodySmall?.color,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
}