Flutter SliverGrid Localization: Grid Layouts in Custom Scroll Views for Multilingual Apps
SliverGrid is a Flutter widget that creates a two-dimensional grid of items inside a CustomScrollView. In multilingual applications, SliverGrid is essential for displaying translated content in responsive grid layouts alongside other slivers, adapting grid columns and aspect ratios for translations of different lengths, supporting RTL grid flow that reverses column order automatically, and combining translated grid sections with sliver headers and lists in a single scroll view.
Understanding SliverGrid in Localization Context
SliverGrid renders children in a grid as a sliver, typically inside a CustomScrollView alongside other slivers. For multilingual apps, this enables:
- Translated grid items in a sliver-based scroll layout
- Adaptive column counts based on screen width and translation length
- RTL-aware grid flow that reverses column order automatically
- Combined layouts with translated grids, lists, and headers
Why SliverGrid Matters for Multilingual Apps
SliverGrid provides:
- Sliver integration: Translated grids that scroll with app bars, lists, and headers
- Flexible delegates:
SliverGridDelegateWithFixedCrossAxisCountandSliverGridDelegateWithMaxCrossAxisExtentfor responsive layouts - Lazy building: Only visible grid items are built for performance
- Custom grid layouts: Adjustable spacing and aspect ratios for different translations
Basic SliverGrid Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedSliverGridExample extends StatelessWidget {
const LocalizedSliverGridExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 150,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text(l10n.categoriesTitle),
),
),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.2,
),
itemCount: 6,
itemBuilder: (context, index) {
final categories = [
(Icons.restaurant, l10n.foodCategoryLabel),
(Icons.shopping_bag, l10n.shoppingCategoryLabel),
(Icons.directions_car, l10n.transportCategoryLabel),
(Icons.movie, l10n.entertainmentCategoryLabel),
(Icons.fitness_center, l10n.healthCategoryLabel),
(Icons.school, l10n.educationCategoryLabel),
];
final (icon, label) = categories[index];
return Card(
child: InkWell(
onTap: () {},
borderRadius: BorderRadius.circular(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 40,
color: Theme.of(context).colorScheme.primary),
const SizedBox(height: 8),
Text(
label,
style: Theme.of(context).textTheme.titleSmall,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
],
),
),
);
},
),
),
],
),
);
}
}
Advanced SliverGrid Patterns for Localization
Responsive Grid with Locale-Aware Column Count
A SliverGrid that adjusts its column count based on screen width and locale verbosity.
class ResponsiveSliverGrid extends StatelessWidget {
const ResponsiveSliverGrid({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final screenWidth = MediaQuery.sizeOf(context).width;
final locale = Localizations.localeOf(context);
final isVerbose = ['de', 'fi', 'hu', 'nl'].contains(locale.languageCode);
final maxExtent = isVerbose ? 200.0 : 160.0;
final products = List.generate(12, (index) {
return _Product(
name: '${l10n.productLabel} ${index + 1}',
price: '\$${((index + 1) * 19.99).toStringAsFixed(2)}',
category: index % 3 == 0
? l10n.electronicsCategory
: index % 3 == 1
? l10n.clothingCategory
: l10n.booksCategory,
);
});
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
title: Text(l10n.productsTitle),
),
SliverPadding(
padding: const EdgeInsets.all(12),
sliver: SliverGrid.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: maxExtent,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
child: const Center(
child: Icon(Icons.image, size: 48),
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: Theme.of(context).textTheme.titleSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
product.category,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(
product.price,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
],
),
);
},
),
),
],
),
);
}
}
class _Product {
final String name;
final String price;
final String category;
_Product({
required this.name,
required this.price,
required this.category,
});
}
Mixed Grid and List Layout
A CustomScrollView combining SliverGrid for featured items and SliverList for regular items.
class MixedGridListLayout extends StatelessWidget {
const MixedGridListLayout({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 180,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text(l10n.exploreTitle),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(16, 16, 16, 8),
child: Text(
l10n.featuredSectionLabel,
style: Theme.of(context).textTheme.titleLarge,
),
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 1.5,
),
itemCount: 4,
itemBuilder: (context, index) {
return Card(
color: Theme.of(context).colorScheme.primaryContainer,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.star, size: 28),
const SizedBox(height: 4),
Text(
'${l10n.featuredItemLabel} ${index + 1}',
style: Theme.of(context).textTheme.labelLarge,
textAlign: TextAlign.center,
),
],
),
),
);
},
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(16, 24, 16, 8),
child: Text(
l10n.allItemsSectionLabel,
style: Theme.of(context).textTheme.titleLarge,
),
),
),
SliverList.separated(
itemCount: 10,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text('${l10n.itemLabel} ${index + 1}'),
subtitle: Text(l10n.itemDescription),
);
},
separatorBuilder: (context, index) => const Divider(height: 1),
),
],
),
);
}
}
Image Gallery Grid
A SliverGrid photo gallery with translated captions and overlay labels.
class GallerySliverGrid extends StatelessWidget {
const GallerySliverGrid({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final photos = List.generate(9, (index) {
return _Photo(
title: '${l10n.photoLabel} ${index + 1}',
location: index % 3 == 0
? l10n.beachLocation
: index % 3 == 1
? l10n.mountainLocation
: l10n.cityLocation,
);
});
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
title: Text(l10n.galleryTitle),
actions: [
IconButton(
icon: const Icon(Icons.filter_list),
tooltip: l10n.filterTooltip,
onPressed: () {},
),
],
),
SliverPadding(
padding: const EdgeInsets.all(8),
sliver: SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
),
itemCount: photos.length,
itemBuilder: (context, index) {
final photo = photos[index];
return Stack(
fit: StackFit.expand,
children: [
Container(
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
child: const Icon(Icons.photo, size: 32),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(4),
color: Colors.black54,
child: Text(
photo.location,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.white,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
),
],
);
},
),
),
],
),
);
}
}
class _Photo {
final String title;
final String location;
_Photo({required this.title, required this.location});
}
RTL Support and Bidirectional Layouts
SliverGrid automatically reverses column order in RTL layouts. Items flow from right to left, and padding adapts when using EdgeInsetsDirectional.
class BidirectionalSliverGrid extends StatelessWidget {
const BidirectionalSliverGrid({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
title: Text(l10n.settingsTitle),
),
SliverPadding(
padding: const EdgeInsetsDirectional.all(16),
sliver: SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
),
itemCount: 4,
itemBuilder: (context, index) {
final settings = [
(Icons.language, l10n.languageLabel),
(Icons.palette, l10n.themeLabel),
(Icons.notifications, l10n.notificationsLabel),
(Icons.security, l10n.privacyLabel),
];
final (icon, label) = settings[index];
return Card(
child: InkWell(
onTap: () {},
borderRadius: BorderRadius.circular(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 36),
const SizedBox(height: 8),
Text(label, textAlign: TextAlign.center),
],
),
),
);
},
),
),
],
),
);
}
}
Testing SliverGrid Localization
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
Widget buildTestWidget({Locale locale = const Locale('en')}) {
return MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const LocalizedSliverGridExample(),
);
}
testWidgets('SliverGrid renders localized items', (tester) async {
await tester.pumpWidget(buildTestWidget());
await tester.pumpAndSettle();
expect(find.byType(CustomScrollView), findsOneWidget);
});
testWidgets('SliverGrid works in RTL', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
}
Best Practices
Use
SliverGridDelegateWithMaxCrossAxisExtentfor responsive grids that adapt column count to screen width and translation length.Adjust
maxCrossAxisExtentfor verbose locales to give grid items more space for longer translated labels.Wrap SliverGrid in
SliverPaddingwithEdgeInsetsDirectionalso grid margins adapt correctly in RTL layouts.Use
textAlign: TextAlign.centerfor grid item labels to keep text visually centered regardless of translation length.Set
maxLinesandoverflow: TextOverflow.ellipsison grid item text to prevent layout overflow with long translations.Combine SliverGrid with SliverList for mixed layouts that show featured translated content in a grid and detailed items in a list.
Conclusion
SliverGrid provides a sliver-based grid widget for CustomScrollView layouts in Flutter. For multilingual apps, it handles translated grid items with responsive column counts, supports RTL grid flow automatically, and integrates with other slivers for rich scrollable layouts. By combining SliverGrid with locale-aware column sizing, mixed grid-list layouts, and photo galleries with translated captions, you can build responsive grid interfaces that display correctly across all supported languages.