Multi-service home grid
Responsive grid of service tiles with icons, optional badges and a data-driven list that adapts column count to width.
FlutterLayoutsfluttergridhomeservices
Code
import 'package:flutter/material.dart';
class ServiceTile {
const ServiceTile({required this.label, required this.icon, required this.route, this.badge, this.enabled = true});
final String label;
final IconData icon;
final String route;
final String? badge; // e.g. "New" or "20% off"
final bool enabled;
}
class ServiceGrid extends StatelessWidget {
const ServiceGrid({super.key, required this.tiles, required this.onOpen});
final List<ServiceTile> tiles;
final void Function(ServiceTile) onOpen;
@override
Widget build(BuildContext context) {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), // lives inside a parent scroll view
padding: const EdgeInsets.all(16),
// Column count follows the width: 4 on phones, more on tablets.
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 96,
mainAxisSpacing: 16,
crossAxisSpacing: 12,
childAspectRatio: 0.82,
),
itemCount: tiles.length,
itemBuilder: (context, i) => _Tile(tile: tiles[i], onTap: () => onOpen(tiles[i])),
);
}
}
class _Tile extends StatelessWidget {
const _Tile({required this.tile, required this.onTap});
final ServiceTile tile;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Semantics(
button: true,
enabled: tile.enabled,
label: tile.label,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: tile.enabled ? onTap : null,
child: Opacity(
opacity: tile.enabled ? 1 : 0.45,
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Stack(clipBehavior: Clip.none, children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(color: scheme.secondaryContainer, borderRadius: BorderRadius.circular(16)),
child: Icon(tile.icon, color: scheme.onSecondaryContainer),
),
if (tile.badge != null)
PositionedDirectional(
top: -6,
end: -8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(color: scheme.error, borderRadius: BorderRadius.circular(8)),
child: Text(tile.badge!, style: TextStyle(fontSize: 10, color: scheme.onError)),
),
),
]),
const SizedBox(height: 6),
Text(tile.label, maxLines: 2, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelMedium),
]),
),
),
);
}
}
How it works
What it solves
Super-app style home screens present many services at once and add or hide them without an app release. A grid built from data, rather than hand-placed widgets, makes that practical.
How it works
ServiceTile is a plain data class with a label, icon, route key, optional badge and enabled flag. ServiceGrid uses SliverGridDelegateWithMaxCrossAxisExtent, so the number of columns is calculated from the available width: four on a typical phone and more on tablets, with no breakpoints.
Each tile is an InkWell with ripple, wrapped in Semantics so screen readers announce a button with its enabled state. Disabled tiles fade and ignore taps; badges are positioned with directional offsets so they stay on the correct corner in right-to-left languages.
Adapting it
Fetch the tile list from remote config and map icon names to IconData. Route with onOpen, for example by pushing a named route or switching a tab. Because the grid uses shrinkWrap, put it inside a ListView or SingleChildScrollView along with banners and carousels.
Pitfalls
shrinkWraplays out all tiles up front; for dozens of items use a sliver grid in aCustomScrollView.- Long translated labels are cut after two lines; verify them in your widest language.
- Keep badge text very short.
How to use it
Copy to lib/home/. No packages.
ServiceGrid(
tiles: const [ServiceTile(label: 'Rides', icon: Icons.local_taxi, route: '/rides', badge: 'New')],
onOpen: (t) => Navigator.of(context).pushNamed(t.route),
)
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.