← Back to Blog

Flutter Row Localization: Building Adaptive Horizontal Layouts for Multilingual Apps

flutterrowlayouthorizontallocalizationrtl

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.start and MainAxisAlignment.end flip based on text direction
  • CrossAxisAlignment.start adapts 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

  1. Always use Expanded or Flexible for text-heavy children in a Row to prevent overflow when translations are longer than expected.

  2. Use EdgeInsetsDirectional instead of EdgeInsets for padding and margins around Row children to ensure spacing mirrors correctly in RTL locales.

  3. Avoid hardcoded widths for text containers inside Row. Different scripts have vastly different character widths.

  4. Test with MainAxisAlignment.start and .end in both LTR and RTL locales, as these alignments flip automatically.

  5. Handle directional icons explicitly when used inside Row. Check Directionality.of(context) and swap icon variants for RTL contexts.

  6. 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.

Further Reading