Flutter AspectRatio Localization: Proportional Sizing for Multilingual Apps
AspectRatio forces its child to have a specific width-to-height ratio, essential for maintaining consistent proportions across different screen sizes and content lengths. In multilingual apps, AspectRatio helps create responsive layouts where media and content containers maintain proper proportions regardless of translated text length or text direction. This guide covers comprehensive strategies for using AspectRatio in Flutter localization.
Understanding AspectRatio in Localization
AspectRatio widgets benefit localization for:
- Media containers: Video players and image frames that maintain proportions
- Card layouts: Product cards with consistent aspect ratios across languages
- Responsive grids: Grid items that scale proportionally with different content
- Cultural adaptations: Different aspect ratios for regional preferences
- Banner displays: Promotional content with fixed proportions
- Avatar frames: Profile images with consistent shapes
Basic AspectRatio with Localized Content
Start with a simple aspect ratio container:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedAspectRatioDemo extends StatelessWidget {
const LocalizedAspectRatioDemo({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.aspectRatioTitle)),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
l10n.videoPlayerLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
// 16:9 video container
AspectRatio(
aspectRatio: 16 / 9,
child: Container(
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12),
),
child: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.play_circle_outline,
size: 64,
color: Colors.white70,
),
Positioned(
bottom: 12,
left: 12,
right: 12,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
l10n.videoTitle,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
l10n.videoDuration('12:34'),
style: const TextStyle(color: Colors.white70),
),
],
),
),
],
),
),
),
const SizedBox(height: 24),
Text(
l10n.thumbnailGridLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
// Square thumbnail grid
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: List.generate(6, (index) {
return AspectRatio(
aspectRatio: 1,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
l10n.thumbnailLabel(index + 1),
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
),
);
}),
),
],
),
),
);
}
}
ARB File Structure for AspectRatio
{
"aspectRatioTitle": "Aspect Ratio Demo",
"@aspectRatioTitle": {
"description": "Title for aspect ratio demo page"
},
"videoPlayerLabel": "Video Player (16:9)",
"videoTitle": "Introduction to Flutter",
"videoDuration": "{duration}",
"@videoDuration": {
"placeholders": {
"duration": {"type": "String"}
}
},
"thumbnailGridLabel": "Thumbnail Grid (1:1)",
"thumbnailLabel": "Item {number}",
"@thumbnailLabel": {
"placeholders": {
"number": {"type": "int"}
}
}
}
Product Cards with Consistent Proportions
Create product cards that maintain aspect ratios:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedProductGrid extends StatelessWidget {
const LocalizedProductGrid({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
final products = [
{
'title': l10n.product1Title,
'price': l10n.product1Price,
'rating': 4.5,
'reviews': l10n.reviewCount(128),
},
{
'title': l10n.product2Title,
'price': l10n.product2Price,
'rating': 4.8,
'reviews': l10n.reviewCount(256),
},
{
'title': l10n.product3Title,
'price': l10n.product3Price,
'rating': 4.2,
'reviews': l10n.reviewCount(64),
},
{
'title': l10n.product4Title,
'price': l10n.product4Price,
'rating': 4.6,
'reviews': l10n.reviewCount(512),
},
];
return Scaffold(
appBar: AppBar(title: Text(l10n.productsTitle)),
body: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 0.7, // Consistent card proportions
),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Product image with fixed aspect ratio
AspectRatio(
aspectRatio: 1,
child: Container(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Stack(
children: [
Center(
child: Icon(
Icons.image,
size: 48,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Positioned(
top: 8,
right: isRtl ? null : 8,
left: isRtl ? 8 : null,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.star,
size: 14,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
product['rating'].toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
),
),
// Product details
Expanded(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product['title'] as String,
style: Theme.of(context).textTheme.titleSmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
product['reviews'] as String,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const Spacer(),
Text(
product['price'] as String,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
);
},
),
);
}
}
Cultural Aspect Ratio Adaptations
Adapt aspect ratios based on regional preferences:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class CulturalAspectRatioAdapter extends StatelessWidget {
const CulturalAspectRatioAdapter({super.key});
// Different regions may prefer different aspect ratios
double _getPreferredAspectRatio(Locale locale) {
switch (locale.languageCode) {
case 'ja': // Japan often uses taller formats
case 'ko': // Korea similar preference
return 9 / 16; // Portrait orientation
case 'ar': // Arabic regions
case 'he': // Hebrew
return 4 / 3; // More traditional ratio
default:
return 16 / 9; // Standard widescreen
}
}
String _getAspectRatioLabel(AppLocalizations l10n, double ratio) {
if (ratio == 16 / 9) return l10n.aspectRatioWidescreen;
if (ratio == 4 / 3) return l10n.aspectRatioStandard;
if (ratio == 9 / 16) return l10n.aspectRatioPortrait;
if (ratio == 1) return l10n.aspectRatioSquare;
return l10n.aspectRatioCustom;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final locale = Localizations.localeOf(context);
final preferredRatio = _getPreferredAspectRatio(locale);
return Scaffold(
appBar: AppBar(title: Text(l10n.culturalAdaptationTitle)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.currentLocaleLabel(locale.languageCode),
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
l10n.preferredAspectRatioLabel(
_getAspectRatioLabel(l10n, preferredRatio),
),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 24),
// Media container with cultural aspect ratio
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: AspectRatio(
aspectRatio: preferredRatio,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary,
],
),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.aspect_ratio,
size: 48,
color: Colors.white,
),
const SizedBox(height: 8),
Text(
_getAspectRatioLabel(l10n, preferredRatio),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
),
),
const SizedBox(height: 32),
// All available ratios
Text(
l10n.availableRatiosLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildRatioOption(context, l10n, 16 / 9, preferredRatio),
_buildRatioOption(context, l10n, 4 / 3, preferredRatio),
_buildRatioOption(context, l10n, 1, preferredRatio),
_buildRatioOption(context, l10n, 9 / 16, preferredRatio),
],
),
],
),
),
);
}
Widget _buildRatioOption(
BuildContext context,
AppLocalizations l10n,
double ratio,
double selectedRatio,
) {
final isSelected = (ratio - selectedRatio).abs() < 0.01;
return Column(
children: [
Container(
width: 50,
decoration: BoxDecoration(
border: Border.all(
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
width: isSelected ? 2 : 1,
),
borderRadius: BorderRadius.circular(4),
),
child: AspectRatio(
aspectRatio: ratio,
child: Container(
color: isSelected
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceVariant,
),
),
),
const SizedBox(height: 4),
Text(
_getAspectRatioLabel(l10n, ratio),
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
],
);
}
}
Banner Carousel with Fixed Proportions
Create a banner carousel maintaining aspect ratios:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedBannerCarousel extends StatefulWidget {
const LocalizedBannerCarousel({super.key});
@override
State<LocalizedBannerCarousel> createState() => _LocalizedBannerCarouselState();
}
class _LocalizedBannerCarouselState extends State<LocalizedBannerCarousel> {
final PageController _pageController = PageController();
int _currentPage = 0;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
final banners = [
{
'title': l10n.banner1Title,
'subtitle': l10n.banner1Subtitle,
'cta': l10n.banner1Cta,
'color': Colors.blue,
},
{
'title': l10n.banner2Title,
'subtitle': l10n.banner2Subtitle,
'cta': l10n.banner2Cta,
'color': Colors.green,
},
{
'title': l10n.banner3Title,
'subtitle': l10n.banner3Subtitle,
'cta': l10n.banner3Cta,
'color': Colors.orange,
},
];
return Scaffold(
appBar: AppBar(title: Text(l10n.bannersTitle)),
body: Column(
children: [
// Banner carousel with fixed aspect ratio
AspectRatio(
aspectRatio: 21 / 9, // Ultra-wide banner
child: PageView.builder(
controller: _pageController,
reverse: isRtl,
onPageChanged: (page) => setState(() => _currentPage = page),
itemCount: banners.length,
itemBuilder: (context, index) {
final banner = banners[index];
return Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: isRtl ? Alignment.centerRight : Alignment.centerLeft,
end: isRtl ? Alignment.centerLeft : Alignment.centerRight,
colors: [
(banner['color'] as Color).withOpacity(0.8),
banner['color'] as Color,
],
),
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.all(24),
child: Row(
children: [
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
banner['title'] as String,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
banner['subtitle'] as String,
style: const TextStyle(color: Colors.white70),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: banner['color'] as Color,
),
child: Text(banner['cta'] as String),
),
],
),
),
Expanded(
child: Icon(
Icons.local_offer,
size: 80,
color: Colors.white24,
),
),
],
),
);
},
),
),
// Page indicators
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(banners.length, (index) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 4),
width: _currentPage == index ? 24 : 8,
height: 8,
decoration: BoxDecoration(
color: _currentPage == index
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outlineVariant,
borderRadius: BorderRadius.circular(4),
),
);
}),
),
const SizedBox(height: 24),
// Content below banners
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.featuredSectionTitle,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Expanded(
child: GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: List.generate(4, (index) {
return AspectRatio(
aspectRatio: 1,
child: Card(
child: Center(
child: Text(l10n.featureItem(index + 1)),
),
),
);
}),
),
),
],
),
),
),
],
),
);
}
}
Complete ARB File for AspectRatio
{
"@@locale": "en",
"aspectRatioTitle": "Aspect Ratio Demo",
"videoPlayerLabel": "Video Player (16:9)",
"videoTitle": "Introduction to Flutter",
"videoDuration": "{duration}",
"@videoDuration": {
"placeholders": {"duration": {"type": "String"}}
},
"thumbnailGridLabel": "Thumbnail Grid (1:1)",
"thumbnailLabel": "Item {number}",
"@thumbnailLabel": {
"placeholders": {"number": {"type": "int"}}
},
"productsTitle": "Products",
"product1Title": "Wireless Bluetooth Headphones",
"product1Price": "$129.99",
"product2Title": "Smart Fitness Watch",
"product2Price": "$249.99",
"product3Title": "Portable Charger",
"product3Price": "$49.99",
"product4Title": "Noise Cancelling Earbuds",
"product4Price": "$179.99",
"reviewCount": "{count} reviews",
"@reviewCount": {
"placeholders": {"count": {"type": "int"}}
},
"culturalAdaptationTitle": "Cultural Adaptation",
"currentLocaleLabel": "Current locale: {locale}",
"@currentLocaleLabel": {
"placeholders": {"locale": {"type": "String"}}
},
"preferredAspectRatioLabel": "Preferred ratio: {ratio}",
"@preferredAspectRatioLabel": {
"placeholders": {"ratio": {"type": "String"}}
},
"availableRatiosLabel": "Available Ratios",
"aspectRatioWidescreen": "16:9",
"aspectRatioStandard": "4:3",
"aspectRatioPortrait": "9:16",
"aspectRatioSquare": "1:1",
"aspectRatioCustom": "Custom",
"bannersTitle": "Promotions",
"banner1Title": "Summer Sale",
"banner1Subtitle": "Up to 50% off on selected items",
"banner1Cta": "Shop Now",
"banner2Title": "New Arrivals",
"banner2Subtitle": "Check out the latest products",
"banner2Cta": "Explore",
"banner3Title": "Free Shipping",
"banner3Subtitle": "On orders over $50",
"banner3Cta": "Learn More",
"featuredSectionTitle": "Featured",
"featureItem": "Feature {number}",
"@featureItem": {
"placeholders": {"number": {"type": "int"}}
}
}
Best Practices Summary
- Use for media containers: Videos, images, and thumbnails benefit from fixed ratios
- Consider cultural preferences: Different regions may prefer different aspect ratios
- Maintain consistency: Use the same aspect ratio for similar content types
- Test with long text: Ensure layouts work when translated text is longer
- Combine with constraints: Use with ConstrainedBox for maximum size limits
- Responsive grids: Set childAspectRatio in GridView for consistent cards
- RTL considerations: Aspect ratio itself is direction-agnostic, but content should adapt
- Performance: AspectRatio is lightweight and doesn't affect performance
- Accessibility: Ensure content remains readable regardless of aspect ratio
- Flexible layouts: Combine with Expanded or Flexible for responsive designs
Conclusion
AspectRatio is essential for maintaining consistent proportions in Flutter layouts, especially when dealing with media content and product displays in multilingual apps. By setting appropriate aspect ratios, you ensure that your UI looks polished and professional across all supported languages and screen sizes.
Remember that while AspectRatio maintains proportions, the content inside should still be localized and may need to adapt to different text lengths and directions. Always test your layouts with various locales to ensure the best user experience.