Flutter Row Localization: Building Adaptive Horizontal Layouts for Multilingual Apps
Row is one of Flutter's most fundamental layout widgets, arranging its children horizontally in a linear sequence. In multilingual applications, Row plays a critical role because it automatically reverses its children's visual order in right-to-left (RTL) locales, adapts its main axis alignment behavior, and must gracefully handle the significant text length variations that occur between languages.
Understanding Row in Localization Context
Row lays out its children along the horizontal axis, respecting the ambient Directionality of the widget tree. For multilingual apps, this creates several important behaviors:
- Children are automatically reversed in RTL locales like Arabic and Hebrew
MainAxisAlignment.startandMainAxisAlignment.endflip based on text directionCrossAxisAlignment.startadapts to vertical alignment needs per script- Spacing and padding must accommodate varying text lengths across languages
Why Row Matters for Multilingual Apps
Row is the primary building block for horizontal layouts, and proper localization ensures:
- Automatic RTL reversal: Children reorder without manual intervention
- Directional alignment: Start and end positions respect locale direction
- Text overflow handling: Layouts adapt when translations are longer or shorter
- Visual consistency: Horizontal arrangements look natural in every language
Basic Row Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedRowExample extends StatelessWidget {
const LocalizedRowExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.account_circle,
size: 48,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.userName,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
Text(
l10n.userStatus,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
],
),
),
FilledButton.tonal(
onPressed: () {},
child: Text(l10n.followButton),
),
],
),
);
}
}
Advanced Row Patterns for Localization
RTL-Aware Row Layouts
Flutter's Row automatically reverses children order in RTL locales. However, when you use directional padding or margins alongside Row, you must use EdgeInsetsDirectional to ensure spacing is mirrored correctly.
class RtlAwareRow extends StatelessWidget {
const RtlAwareRow({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Container(
padding: const EdgeInsetsDirectional.only(
start: 16,
end: 8,
top: 12,
bottom: 12,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
shape: BoxShape.circle,
),
child: Icon(
Icons.notifications_active,
color: Theme.of(context).colorScheme.primary,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
l10n.notificationMessage,
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () {},
tooltip: l10n.dismissTooltip,
),
],
),
);
}
}
Adaptive Spacing for Different Text Lengths
Languages like German and Finnish produce significantly longer translations than English or Chinese. Use Flexible and Expanded within Row to prevent overflow when text grows.
class AdaptiveSpacingRow extends StatelessWidget {
final String label;
final String value;
const AdaptiveSpacingRow({
super.key,
required this.label,
required this.value,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Flexible(
flex: 2,
child: Text(
label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 16),
Flexible(
flex: 3,
child: Text(
value,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.end,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}
Direction-Aware Main Axis Alignment
MainAxisAlignment.start and MainAxisAlignment.end automatically flip in RTL contexts. This means a Row with MainAxisAlignment.start places children on the right in Arabic.
class DirectionAwareAlignment extends StatelessWidget {
const DirectionAwareAlignment({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
Text(l10n.taskCompleted),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_StatusChip(label: l10n.statusActive, color: Colors.green),
_StatusChip(label: l10n.statusPending, color: Colors.orange),
_StatusChip(label: l10n.statusClosed, color: Colors.red),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {},
child: Text(l10n.cancelButton),
),
const SizedBox(width: 8),
FilledButton(
onPressed: () {},
child: Text(l10n.saveButton),
),
],
),
],
);
}
}
class _StatusChip extends StatelessWidget {
final String label;
final Color color;
const _StatusChip({required this.label, required this.color});
@override
Widget build(BuildContext context) {
return Chip(
label: Text(label),
backgroundColor: color.withOpacity(0.1),
labelStyle: TextStyle(color: color, fontWeight: FontWeight.w500),
side: BorderSide.none,
);
}
}
RTL Support and Bidirectional Layouts
Row's built-in RTL support handles most directional scenarios automatically. However, some cases require explicit handling, such as when you mix directional icons with text.
class BidirectionalRowLayout extends StatelessWidget {
const BidirectionalRowLayout({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
l10n.previousStep,
style: Theme.of(context).textTheme.bodyMedium,
),
),
Icon(
isRtl ? Icons.arrow_back : Icons.arrow_forward,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
l10n.nextStep,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Text(l10n.progressLabel),
const SizedBox(width: 12),
Expanded(
child: LinearProgressIndicator(
value: 0.65,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(width: 12),
Text(
'65%',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
);
}
}
Testing Row 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 buildTestableWidget({
required Locale locale,
required Widget child,
}) {
return MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(body: child),
);
}
group('Row localization tests', () {
testWidgets('Row children order reverses in RTL', (tester) async {
await tester.pumpWidget(
buildTestableWidget(
locale: const Locale('ar'),
child: const Row(
children: [Text('First'), Text('Second')],
),
),
);
final firstOffset = tester.getTopLeft(find.text('First'));
final secondOffset = tester.getTopLeft(find.text('Second'));
expect(firstOffset.dx, greaterThan(secondOffset.dx));
});
testWidgets('MainAxisAlignment.start respects text direction',
(tester) async {
await tester.pumpWidget(
buildTestableWidget(
locale: const Locale('ar'),
child: const Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [Icon(Icons.star)],
),
),
);
final iconOffset = tester.getTopLeft(find.byIcon(Icons.star));
final screenWidth =
tester.view.physicalSize.width / tester.view.devicePixelRatio;
expect(iconOffset.dx, greaterThan(screenWidth / 2));
});
});
}
Best Practices
Always use Expanded or Flexible for text-heavy children in a Row to prevent overflow when translations are longer than expected.
Use EdgeInsetsDirectional instead of EdgeInsets for padding and margins around Row children to ensure spacing mirrors correctly in RTL locales.
Avoid hardcoded widths for text containers inside Row. Different scripts have vastly different character widths.
Test with MainAxisAlignment.start and .end in both LTR and RTL locales, as these alignments flip automatically.
Handle directional icons explicitly when used inside Row. Check
Directionality.of(context)and swap icon variants for RTL contexts.Set maxLines and overflow on Text widgets within Row to gracefully handle translations that exceed available space.
Conclusion
Row is the backbone of horizontal layout in Flutter, and its built-in localization support makes it a powerful tool for multilingual apps. By understanding how Row automatically reverses children in RTL contexts, how MainAxisAlignment adapts to text direction, and how to prevent overflow with varying text lengths, you can build robust horizontal layouts that work seamlessly across all supported languages.