aligned_grid_view 0.1.0
aligned_grid_view: ^0.1.0 copied to clipboard
Aligned grid widgets for Flutter that size every tile in a row equally.
import 'package:aligned_grid_view/aligned_grid_view.dart';
import 'package:flutter/material.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Aligned Grid View',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xff2364aa)),
useMaterial3: true,
),
home: const ExampleIndexPage(),
);
}
}
class ExampleIndexPage extends StatelessWidget {
const ExampleIndexPage({super.key});
@override
Widget build(BuildContext context) {
final examples = [
_Example(
title: 'Fixed columns',
description: 'AlignedGridView.count with three columns.',
icon: Icons.view_module_outlined,
page: const CountExamplePage(),
),
_Example(
title: 'Responsive extent',
description: 'AlignedGridView.extent adapts its column count.',
icon: Icons.aspect_ratio_outlined,
page: const ExtentExamplePage(),
),
_Example(
title: 'Custom scroll view',
description: 'SliverAlignedGrid alongside other slivers.',
icon: Icons.view_stream_outlined,
page: const SliverExamplePage(),
),
_Example(
title: 'Large data set',
description: '1,000 varied tiles built lazily as you scroll.',
icon: Icons.dataset_outlined,
page: const LargeDataSetExamplePage(),
),
];
return Scaffold(
appBar: AppBar(title: const Text('Aligned Grid View')),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Examples',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 8),
const Text('Each row takes the height of its tallest tile.'),
const SizedBox(height: 24),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
final crossAxisCount = constraints.maxWidth >= 700 ? 3 : 1;
if (crossAxisCount == 1) {
return ListView.separated(
itemCount: examples.length,
separatorBuilder: (context, index) =>
const SizedBox(height: 16),
itemBuilder: (context, index) =>
_ExampleTile(example: examples[index]),
);
}
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 0.7,
),
itemCount: examples.length,
itemBuilder: (context, index) =>
_ExampleTile(example: examples[index]),
);
},
),
),
],
),
),
),
);
}
}
class _ExampleTile extends StatelessWidget {
const _ExampleTile({required this.example});
final _Example example;
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => Navigator.of(
context,
).push(MaterialPageRoute<void>(builder: (context) => example.page)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
example.icon,
size: 32,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 24),
Text(
example.title,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(example.description),
const SizedBox(height: 12),
const Align(
alignment: Alignment.centerRight,
child: Icon(Icons.arrow_forward),
),
],
),
),
),
);
}
}
class CountExamplePage extends StatelessWidget {
const CountExamplePage({super.key});
@override
Widget build(BuildContext context) {
return _ExampleScaffold(
title: 'Fixed columns',
description: 'Three tiles per row, each row aligned to its tallest tile.',
child: AlignedGridView.count(
padding: const EdgeInsets.all(20),
crossAxisCount: 3,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
itemCount: _sampleCards.length,
itemBuilder: (context, index) => _SampleCard(card: _sampleCards[index]),
),
);
}
}
class ExtentExamplePage extends StatelessWidget {
const ExtentExamplePage({super.key});
@override
Widget build(BuildContext context) {
return _ExampleScaffold(
title: 'Responsive extent',
description:
'Tiles stay below 240 pixels wide while columns adapt to space.',
child: AlignedGridView.extent(
padding: const EdgeInsets.all(20),
maxCrossAxisExtent: 240,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
itemCount: _sampleCards.length,
itemBuilder: (context, index) => _SampleCard(card: _sampleCards[index]),
),
);
}
}
class SliverExamplePage extends StatelessWidget {
const SliverExamplePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Custom scroll view')),
body: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
sliver: SliverToBoxAdapter(
child: Text(
'SliverAlignedGrid works with headers, lists, and other slivers.',
style: Theme.of(context).textTheme.bodyLarge,
),
),
),
SliverPadding(
padding: const EdgeInsets.all(20),
sliver: SliverAlignedGrid.extent(
maxCrossAxisExtent: 240,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
itemCount: _sampleCards.length,
itemBuilder: (context, index) =>
_SampleCard(card: _sampleCards[index]),
),
),
],
),
);
}
}
class LargeDataSetExamplePage extends StatelessWidget {
const LargeDataSetExamplePage({super.key});
@override
Widget build(BuildContext context) {
return _ExampleScaffold(
title: 'Large data set',
description:
'1,000 items built lazily with short, medium, and tall rows.',
child: AlignedGridView.extent(
padding: const EdgeInsets.all(20),
maxCrossAxisExtent: 240,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
addAutomaticKeepAlives: false,
itemCount: 1000,
itemBuilder: (context, index) => _SampleCard(card: _largeCardAt(index)),
),
);
}
}
class _ExampleScaffold extends StatelessWidget {
const _ExampleScaffold({
required this.title,
required this.description,
required this.child,
});
final String title;
final String description;
final Widget child;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 4),
child: Text(
description,
style: Theme.of(context).textTheme.bodyLarge,
),
),
Expanded(child: child),
],
),
);
}
}
class _SampleCard extends StatelessWidget {
const _SampleCard({required this.card});
final _SampleCardData card;
@override
Widget build(BuildContext context) {
return Card(
color: card.color,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(card.icon),
const SizedBox(height: 24),
Text(card.title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text(card.description),
],
),
),
);
}
}
class _Example {
const _Example({
required this.title,
required this.description,
required this.icon,
required this.page,
});
final String title;
final String description;
final IconData icon;
final Widget page;
}
class _SampleCardData {
const _SampleCardData({
required this.title,
required this.description,
required this.icon,
required this.color,
});
final String title;
final String description;
final IconData icon;
final Color color;
}
const _sampleCards = [
_SampleCardData(
title: 'Short note',
description: 'A compact tile.',
icon: Icons.lightbulb_outline,
color: Color(0xfffff4cc),
),
_SampleCardData(
title: 'Longer content',
description:
'This tile has a longer explanation, so the whole row grows to match it.',
icon: Icons.subject_outlined,
color: Color(0xffdff3e4),
),
_SampleCardData(
title: 'Status',
description: 'Ready for review.',
icon: Icons.check_circle_outline,
color: Color(0xffdcecff),
),
_SampleCardData(
title: 'Another item',
description: 'Rows are independent from one another.',
icon: Icons.layers_outlined,
color: Color(0xffffe4d6),
),
_SampleCardData(
title: 'Detailed card',
description:
'Content can differ substantially in length without leaving uneven tile bottoms in a row.',
icon: Icons.format_align_left,
color: Color(0xffeee1ff),
),
_SampleCardData(
title: 'Final tile',
description: 'The last row can contain fewer tiles.',
icon: Icons.flag_outlined,
color: Color(0xffd9f5f0),
),
];
_SampleCardData _largeCardAt(int index) {
const titles = [
'Quick update',
'Project note',
'Customer feedback',
'Release checklist',
'Research summary',
'Team reminder',
];
const icons = [
Icons.bolt_outlined,
Icons.note_alt_outlined,
Icons.forum_outlined,
Icons.task_alt_outlined,
Icons.science_outlined,
Icons.groups_outlined,
];
const colors = [
Color(0xfffff4cc),
Color(0xffdff3e4),
Color(0xffdcecff),
Color(0xffffe4d6),
Color(0xffeee1ff),
Color(0xffd9f5f0),
];
final description = switch (index % 12) {
0 =>
'This deliberately detailed entry represents a substantial update with several useful points for reviewers. It makes this row noticeably taller than the surrounding rows.',
1 =>
'This entry has a little more detail than a short note, but remains more compact than the detailed update nearby.',
_ => 'A short piece of information.',
};
return _SampleCardData(
title: '${titles[index % titles.length]} ${index + 1}',
description: description,
icon: icons[index % icons.length],
color: colors[index % colors.length],
);
}