Flutter Spacer Localization: Flexible Spacing for Multilingual Layouts
Spacer is Flutter's widget for creating flexible empty space in Flex containers like Row and Column. In multilingual applications, Spacer becomes essential for creating layouts that adapt gracefully to varying text lengths while maintaining proper alignment and spacing.
Understanding Spacer in Localization Context
Spacer expands to fill available space in a Flex container, pushing other widgets apart. For multilingual apps, this is valuable because:
- Text lengths vary dramatically between languages
- Flexible spacing adapts to content without overflow
- RTL layouts work seamlessly with Spacer
- UI elements maintain proper distribution regardless of translation length
Why Spacer Matters for Multilingual Apps
Proper use of Spacer ensures:
- Adaptive layouts: Content spreads evenly regardless of text length
- RTL compatibility: Spacer works identically in both directions
- Visual balance: Elements maintain pleasing distribution
- Overflow prevention: Flexible spacing prevents layout breaks
Basic Spacer Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedSpacerExample extends StatelessWidget {
const LocalizedSpacerExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Text(
l10n.welcomeLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const Spacer(),
ElevatedButton(
onPressed: () {},
child: Text(l10n.actionButton),
),
],
),
);
}
}
Header with Spacer
Localized App Header
class LocalizedHeader extends StatelessWidget {
final String title;
final List<Widget>? actions;
const LocalizedHeader({
super.key,
required this.title,
this.actions,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
if (actions != null) ...actions!,
],
),
);
}
}
// Usage
class HeaderExample extends StatelessWidget {
const HeaderExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return LocalizedHeader(
title: l10n.dashboardTitle,
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {},
tooltip: l10n.searchTooltip,
),
IconButton(
icon: const Icon(Icons.notifications_outlined),
onPressed: () {},
tooltip: l10n.notificationsTooltip,
),
],
);
}
}
List Item with Spacer
Balanced List Items
class LocalizedListItem extends StatelessWidget {
final IconData icon;
final String title;
final String? trailing;
final VoidCallback? onTap;
const LocalizedListItem({
super.key,
required this.icon,
required this.title,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Icon(
icon,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 16),
Text(
title,
style: Theme.of(context).textTheme.bodyLarge,
),
const Spacer(),
if (trailing != null)
Text(
trailing!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(width: 8),
Icon(
Icons.chevron_right,
color: Theme.of(context).colorScheme.outline,
),
],
),
),
);
}
}
// Usage
class SettingsMenu extends StatelessWidget {
const SettingsMenu({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
LocalizedListItem(
icon: Icons.language,
title: l10n.languageSetting,
trailing: l10n.currentLanguage,
onTap: () {},
),
LocalizedListItem(
icon: Icons.notifications,
title: l10n.notificationsSetting,
trailing: l10n.enabledStatus,
onTap: () {},
),
LocalizedListItem(
icon: Icons.dark_mode,
title: l10n.themeSetting,
trailing: l10n.systemDefault,
onTap: () {},
),
],
);
}
}
Flex Distribution with Multiple Spacers
Evenly Distributed Actions
class DistributedActions extends StatelessWidget {
const DistributedActions({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
_ActionButton(
icon: Icons.share,
label: l10n.shareAction,
onPressed: () {},
),
const Spacer(),
_ActionButton(
icon: Icons.bookmark_outline,
label: l10n.saveAction,
onPressed: () {},
),
const Spacer(),
_ActionButton(
icon: Icons.download,
label: l10n.downloadAction,
onPressed: () {},
),
],
),
);
}
}
class _ActionButton extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onPressed;
const _ActionButton({
required this.icon,
required this.label,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon),
const SizedBox(height: 4),
Text(
label,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
);
}
}
Weighted Spacing with Flex
Proportional Space Distribution
class WeightedSpacingExample extends StatelessWidget {
const WeightedSpacingExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Label takes minimum space needed
Text(l10n.statusLabel),
// Small space after label
const Spacer(flex: 1),
// Status indicator
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
l10n.activeStatus,
style: const TextStyle(color: Colors.green),
),
),
// Larger space before action
const Spacer(flex: 2),
// Action button
TextButton(
onPressed: () {},
child: Text(l10n.viewDetailsButton),
),
],
),
);
}
}
Card Footer with Spacer
Localized Card Actions
class LocalizedCardWithFooter extends StatelessWidget {
final String title;
final String description;
final String date;
final VoidCallback? onPrimary;
final VoidCallback? onSecondary;
const LocalizedCardWithFooter({
super.key,
required this.title,
required this.description,
required this.date,
this.onPrimary,
this.onSecondary,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
description,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
date,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
),
const Spacer(),
if (onSecondary != null)
TextButton(
onPressed: onSecondary,
child: Text(l10n.cancelButton),
),
if (onPrimary != null)
TextButton(
onPressed: onPrimary,
child: Text(l10n.confirmButton),
),
],
),
),
],
),
);
}
}
Navigation Bar with Spacer
Flexible Navigation Layout
class LocalizedNavBar extends StatelessWidget {
final int selectedIndex;
final ValueChanged<int> onDestinationSelected;
const LocalizedNavBar({
super.key,
required this.selectedIndex,
required this.onDestinationSelected,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: SafeArea(
child: Row(
children: [
_NavItem(
icon: Icons.home_outlined,
selectedIcon: Icons.home,
label: l10n.navHome,
isSelected: selectedIndex == 0,
onTap: () => onDestinationSelected(0),
),
const Spacer(),
_NavItem(
icon: Icons.search_outlined,
selectedIcon: Icons.search,
label: l10n.navSearch,
isSelected: selectedIndex == 1,
onTap: () => onDestinationSelected(1),
),
const Spacer(),
_NavItem(
icon: Icons.add_circle_outline,
selectedIcon: Icons.add_circle,
label: l10n.navCreate,
isSelected: selectedIndex == 2,
onTap: () => onDestinationSelected(2),
),
const Spacer(),
_NavItem(
icon: Icons.person_outline,
selectedIcon: Icons.person,
label: l10n.navProfile,
isSelected: selectedIndex == 3,
onTap: () => onDestinationSelected(3),
),
],
),
),
);
}
}
class _NavItem extends StatelessWidget {
final IconData icon;
final IconData selectedIcon;
final String label;
final bool isSelected;
final VoidCallback onTap;
const _NavItem({
required this.icon,
required this.selectedIcon,
required this.label,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final color = isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isSelected ? selectedIcon : icon,
color: color,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
],
),
),
);
}
}
Price Row with Spacer
Localized Price Display
class LocalizedPriceRow extends StatelessWidget {
final String label;
final String amount;
final bool isTotal;
const LocalizedPriceRow({
super.key,
required this.label,
required this.amount,
this.isTotal = false,
});
@override
Widget build(BuildContext context) {
final textStyle = isTotal
? Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
)
: Theme.of(context).textTheme.bodyMedium;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Text(label, style: textStyle),
const Spacer(),
Text(amount, style: textStyle),
],
),
);
}
}
// Usage
class OrderSummary extends StatelessWidget {
const OrderSummary({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.orderSummaryTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
LocalizedPriceRow(
label: l10n.subtotalLabel,
amount: l10n.subtotalAmount,
),
LocalizedPriceRow(
label: l10n.shippingLabel,
amount: l10n.shippingAmount,
),
LocalizedPriceRow(
label: l10n.taxLabel,
amount: l10n.taxAmount,
),
const Divider(),
LocalizedPriceRow(
label: l10n.totalLabel,
amount: l10n.totalAmount,
isTotal: true,
),
],
),
),
);
}
}
Toolbar with Spacer
Localized Editor Toolbar
class LocalizedToolbar extends StatelessWidget {
const LocalizedToolbar({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
_ToolbarButton(
icon: Icons.format_bold,
tooltip: l10n.boldTooltip,
onPressed: () {},
),
_ToolbarButton(
icon: Icons.format_italic,
tooltip: l10n.italicTooltip,
onPressed: () {},
),
_ToolbarButton(
icon: Icons.format_underlined,
tooltip: l10n.underlineTooltip,
onPressed: () {},
),
const VerticalDivider(width: 16),
_ToolbarButton(
icon: Icons.format_list_bulleted,
tooltip: l10n.bulletListTooltip,
onPressed: () {},
),
_ToolbarButton(
icon: Icons.format_list_numbered,
tooltip: l10n.numberedListTooltip,
onPressed: () {},
),
const Spacer(),
_ToolbarButton(
icon: Icons.undo,
tooltip: l10n.undoTooltip,
onPressed: () {},
),
_ToolbarButton(
icon: Icons.redo,
tooltip: l10n.redoTooltip,
onPressed: () {},
),
],
),
);
}
}
class _ToolbarButton extends StatelessWidget {
final IconData icon;
final String tooltip;
final VoidCallback onPressed;
const _ToolbarButton({
required this.icon,
required this.tooltip,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(icon, size: 20),
tooltip: tooltip,
onPressed: onPressed,
visualDensity: VisualDensity.compact,
);
}
}
ARB File Structure
English (app_en.arb)
{
"@@locale": "en",
"welcomeLabel": "Welcome back!",
"@welcomeLabel": {
"description": "Welcome greeting label"
},
"actionButton": "Get Started",
"@actionButton": {
"description": "Primary action button"
},
"dashboardTitle": "Dashboard",
"searchTooltip": "Search",
"notificationsTooltip": "Notifications",
"languageSetting": "Language",
"currentLanguage": "English",
"notificationsSetting": "Notifications",
"enabledStatus": "Enabled",
"themeSetting": "Theme",
"systemDefault": "System",
"shareAction": "Share",
"saveAction": "Save",
"downloadAction": "Download",
"statusLabel": "Status",
"activeStatus": "Active",
"viewDetailsButton": "View Details",
"cancelButton": "Cancel",
"confirmButton": "Confirm",
"navHome": "Home",
"navSearch": "Search",
"navCreate": "Create",
"navProfile": "Profile",
"orderSummaryTitle": "Order Summary",
"subtotalLabel": "Subtotal",
"subtotalAmount": "$99.00",
"shippingLabel": "Shipping",
"shippingAmount": "$5.99",
"taxLabel": "Tax",
"taxAmount": "$8.40",
"totalLabel": "Total",
"totalAmount": "$113.39",
"boldTooltip": "Bold",
"italicTooltip": "Italic",
"underlineTooltip": "Underline",
"bulletListTooltip": "Bullet List",
"numberedListTooltip": "Numbered List",
"undoTooltip": "Undo",
"redoTooltip": "Redo"
}
German (app_de.arb)
{
"@@locale": "de",
"welcomeLabel": "Willkommen zurück!",
"actionButton": "Loslegen",
"dashboardTitle": "Übersicht",
"searchTooltip": "Suchen",
"notificationsTooltip": "Benachrichtigungen",
"languageSetting": "Sprache",
"currentLanguage": "Deutsch",
"notificationsSetting": "Benachrichtigungen",
"enabledStatus": "Aktiviert",
"themeSetting": "Design",
"systemDefault": "System",
"shareAction": "Teilen",
"saveAction": "Speichern",
"downloadAction": "Herunterladen",
"statusLabel": "Status",
"activeStatus": "Aktiv",
"viewDetailsButton": "Details anzeigen",
"cancelButton": "Abbrechen",
"confirmButton": "Bestätigen",
"navHome": "Startseite",
"navSearch": "Suchen",
"navCreate": "Erstellen",
"navProfile": "Profil",
"orderSummaryTitle": "Bestellübersicht",
"subtotalLabel": "Zwischensumme",
"subtotalAmount": "99,00 €",
"shippingLabel": "Versand",
"shippingAmount": "5,99 €",
"taxLabel": "MwSt.",
"taxAmount": "8,40 €",
"totalLabel": "Gesamt",
"totalAmount": "113,39 €",
"boldTooltip": "Fett",
"italicTooltip": "Kursiv",
"underlineTooltip": "Unterstrichen",
"bulletListTooltip": "Aufzählung",
"numberedListTooltip": "Nummerierte Liste",
"undoTooltip": "Rückgängig",
"redoTooltip": "Wiederholen"
}
Arabic (app_ar.arb)
{
"@@locale": "ar",
"welcomeLabel": "مرحباً بعودتك!",
"actionButton": "ابدأ الآن",
"dashboardTitle": "لوحة التحكم",
"searchTooltip": "بحث",
"notificationsTooltip": "الإشعارات",
"languageSetting": "اللغة",
"currentLanguage": "العربية",
"notificationsSetting": "الإشعارات",
"enabledStatus": "مفعّل",
"themeSetting": "المظهر",
"systemDefault": "النظام",
"shareAction": "مشاركة",
"saveAction": "حفظ",
"downloadAction": "تنزيل",
"statusLabel": "الحالة",
"activeStatus": "نشط",
"viewDetailsButton": "عرض التفاصيل",
"cancelButton": "إلغاء",
"confirmButton": "تأكيد",
"navHome": "الرئيسية",
"navSearch": "بحث",
"navCreate": "إنشاء",
"navProfile": "الملف الشخصي",
"orderSummaryTitle": "ملخص الطلب",
"subtotalLabel": "المجموع الفرعي",
"subtotalAmount": "٩٩٫٠٠ دولار",
"shippingLabel": "الشحن",
"shippingAmount": "٥٫٩٩ دولار",
"taxLabel": "الضريبة",
"taxAmount": "٨٫٤٠ دولار",
"totalLabel": "الإجمالي",
"totalAmount": "١١٣٫٣٩ دولار",
"boldTooltip": "عريض",
"italicTooltip": "مائل",
"underlineTooltip": "تسطير",
"bulletListTooltip": "قائمة نقطية",
"numberedListTooltip": "قائمة مرقمة",
"undoTooltip": "تراجع",
"redoTooltip": "إعادة"
}
Best Practices Summary
Do's
- Use Spacer for flexible gaps instead of fixed SizedBox when possible
- Combine with Expanded for complex flex distributions
- Use flex parameter for proportional spacing
- Test with long translations to verify layout adaptation
- Trust Spacer in RTL - it works identically in both directions
Don'ts
- Don't use Spacer outside Flex containers - it only works in Row/Column
- Don't overuse Spacer when fixed spacing is more appropriate
- Don't forget minimum widths for content that shouldn't shrink
- Don't assume equal distribution - verify with different text lengths
Accessibility Considerations
class AccessibleSpacerLayout extends StatelessWidget {
const AccessibleSpacerLayout({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Semantics(
container: true,
child: Row(
children: [
Semantics(
header: true,
child: Text(l10n.sectionTitle),
),
const Spacer(),
Semantics(
button: true,
label: l10n.actionButtonLabel,
child: ElevatedButton(
onPressed: () {},
child: Text(l10n.actionButton),
),
),
],
),
);
}
}
Conclusion
Spacer is an elegant solution for creating flexible, adaptive layouts in multilingual Flutter applications. By pushing widgets apart with expandable space, it ensures your UI remains balanced and visually pleasing regardless of text length or language direction. Use Spacer when you need proportional distribution and let Flutter's flex system handle the complexity of different content sizes.