← Back to Blog

Flutter Flexible Localization: Adaptive Layouts for Global Apps

flutterflexibleflexlayoutlocalizationresponsive

Flutter Flexible Localization: Adaptive Layouts for Global Apps

Flexible is a Flutter widget that controls how a child flexes within a Row, Column, or Flex. Unlike Expanded, Flexible allows its child to be smaller than the available space. In multilingual applications, Flexible provides precise control over how content areas share and adapt to available space.

Understanding Flexible in Localization Context

Flexible gives its child the ability to expand to fill available space but doesn't require it. For multilingual apps, this distinction is crucial:

  • Content can be its natural size when space permits
  • Long translations can expand when needed
  • RTL layouts maintain proper proportional behavior
  • Different language lengths are handled gracefully

Why Flexible Matters for Multilingual Apps

Adaptive flexibility ensures:

  • Natural sizing: Content uses only the space it needs
  • Graceful expansion: Long text can grow when available
  • Consistent layouts: Visual harmony across languages
  • Responsive design: Layouts adapt to screen sizes

Basic Flexible Implementation

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class LocalizedFlexibleExample extends StatelessWidget {
  const LocalizedFlexibleExample({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Row(
      children: [
        Flexible(
          child: Text(
            l10n.shortMessage,
            overflow: TextOverflow.ellipsis,
          ),
        ),
        const SizedBox(width: 8),
        TextButton(
          onPressed: () {},
          child: Text(l10n.actionButton),
        ),
      ],
    );
  }
}

Flexible vs Expanded

Visual Comparison

class FlexibleVsExpandedDemo extends StatelessWidget {
  const FlexibleVsExpandedDemo({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        // With Flexible - takes only needed space
        Text(
          l10n.flexibleLabel,
          style: Theme.of(context).textTheme.labelMedium,
        ),
        const SizedBox(height: 8),
        Container(
          color: Colors.grey.shade200,
          padding: const EdgeInsets.all(8),
          child: Row(
            children: [
              Flexible(
                child: Container(
                  color: Colors.blue.shade200,
                  padding: const EdgeInsets.all(8),
                  child: Text(l10n.shortText),
                ),
              ),
              const SizedBox(width: 8),
              Container(
                color: Colors.green.shade200,
                padding: const EdgeInsets.all(8),
                child: Text(l10n.fixedContent),
              ),
            ],
          ),
        ),
        const SizedBox(height: 24),
        // With Expanded - takes all available space
        Text(
          l10n.expandedLabel,
          style: Theme.of(context).textTheme.labelMedium,
        ),
        const SizedBox(height: 8),
        Container(
          color: Colors.grey.shade200,
          padding: const EdgeInsets.all(8),
          child: Row(
            children: [
              Expanded(
                child: Container(
                  color: Colors.blue.shade200,
                  padding: const EdgeInsets.all(8),
                  child: Text(l10n.shortText),
                ),
              ),
              const SizedBox(width: 8),
              Container(
                color: Colors.green.shade200,
                padding: const EdgeInsets.all(8),
                child: Text(l10n.fixedContent),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

Tag and Chip Layouts

Flexible Tag Row

class LocalizedTagRow extends StatelessWidget {
  final List<String> tags;
  final int maxVisibleTags;

  const LocalizedTagRow({
    super.key,
    required this.tags,
    this.maxVisibleTags = 3,
  });

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final visibleTags = tags.take(maxVisibleTags).toList();
    final remainingCount = tags.length - maxVisibleTags;

    return Row(
      children: [
        Flexible(
          child: Wrap(
            spacing: 8,
            runSpacing: 4,
            children: visibleTags.map((tag) => _Tag(label: tag)).toList(),
          ),
        ),
        if (remainingCount > 0) ...[
          const SizedBox(width: 8),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
            decoration: BoxDecoration(
              color: Theme.of(context).colorScheme.surfaceContainerHighest,
              borderRadius: BorderRadius.circular(12),
            ),
            child: Text(
              l10n.moreItems(remainingCount),
              style: Theme.of(context).textTheme.labelSmall,
            ),
          ),
        ],
      ],
    );
  }
}

class _Tag extends StatelessWidget {
  final String label;

  const _Tag({required this.label});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.primaryContainer,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Text(
        label,
        style: Theme.of(context).textTheme.labelMedium?.copyWith(
          color: Theme.of(context).colorScheme.onPrimaryContainer,
        ),
      ),
    );
  }
}

Filter Chips with Flexible

class LocalizedFilterChips extends StatefulWidget {
  const LocalizedFilterChips({super.key});

  @override
  State<LocalizedFilterChips> createState() => _LocalizedFilterChipsState();
}

class _LocalizedFilterChipsState extends State<LocalizedFilterChips> {
  final Set<String> _selected = {};

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    final filters = [
      l10n.filterAll,
      l10n.filterActive,
      l10n.filterCompleted,
      l10n.filterPending,
    ];

    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Row(
        children: filters.map((filter) {
          final isSelected = _selected.contains(filter);
          return Padding(
            padding: const EdgeInsetsDirectional.only(end: 8),
            child: Flexible(
              child: FilterChip(
                label: Text(filter),
                selected: isSelected,
                onSelected: (selected) {
                  setState(() {
                    if (selected) {
                      _selected.add(filter);
                    } else {
                      _selected.remove(filter);
                    }
                  });
                },
              ),
            ),
          );
        }).toList(),
      ),
    );
  }
}

Card Content Layouts

Flexible Card Content

class LocalizedFlexibleCard extends StatelessWidget {
  final IconData icon;
  final String title;
  final String description;
  final String? actionLabel;
  final VoidCallback? onAction;

  const LocalizedFlexibleCard({
    super.key,
    required this.icon,
    required this.title,
    required this.description,
    this.actionLabel,
    this.onAction,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.primaryContainer,
                borderRadius: BorderRadius.circular(12),
              ),
              child: Icon(
                icon,
                color: Theme.of(context).colorScheme.primary,
                size: 24,
              ),
            ),
            const SizedBox(width: 16),
            Flexible(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    title,
                    style: Theme.of(context).textTheme.titleMedium?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    description,
                    style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                      color: Theme.of(context).colorScheme.outline,
                    ),
                  ),
                  if (actionLabel != null && onAction != null) ...[
                    const SizedBox(height: 12),
                    TextButton(
                      onPressed: onAction,
                      style: TextButton.styleFrom(
                        padding: EdgeInsets.zero,
                        minimumSize: Size.zero,
                        tapTargetSize: MaterialTapTargetSize.shrinkWrap,
                      ),
                      child: Text(actionLabel!),
                    ),
                  ],
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Notification and Alert Layouts

Flexible Alert Banner

class LocalizedAlertBanner extends StatelessWidget {
  final IconData icon;
  final String message;
  final String? actionLabel;
  final VoidCallback? onAction;
  final VoidCallback? onDismiss;
  final Color? backgroundColor;

  const LocalizedAlertBanner({
    super.key,
    required this.icon,
    required this.message,
    this.actionLabel,
    this.onAction,
    this.onDismiss,
    this.backgroundColor,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      decoration: BoxDecoration(
        color: backgroundColor ?? Theme.of(context).colorScheme.primaryContainer,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Row(
        children: [
          Icon(
            icon,
            size: 20,
            color: Theme.of(context).colorScheme.onPrimaryContainer,
          ),
          const SizedBox(width: 12),
          Flexible(
            child: Text(
              message,
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                color: Theme.of(context).colorScheme.onPrimaryContainer,
              ),
            ),
          ),
          if (actionLabel != null && onAction != null) ...[
            const SizedBox(width: 12),
            TextButton(
              onPressed: onAction,
              style: TextButton.styleFrom(
                minimumSize: Size.zero,
                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
              ),
              child: Text(actionLabel!),
            ),
          ],
          if (onDismiss != null) ...[
            const SizedBox(width: 4),
            IconButton(
              icon: const Icon(Icons.close, size: 18),
              onPressed: onDismiss,
              padding: EdgeInsets.zero,
              constraints: const BoxConstraints(),
            ),
          ],
        ],
      ),
    );
  }
}

// Usage
class NotificationExample extends StatelessWidget {
  const NotificationExample({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Column(
      children: [
        LocalizedAlertBanner(
          icon: Icons.info_outline,
          message: l10n.updateAvailableMessage,
          actionLabel: l10n.updateNow,
          onAction: () {},
          onDismiss: () {},
        ),
        const SizedBox(height: 12),
        LocalizedAlertBanner(
          icon: Icons.warning_amber_outlined,
          message: l10n.lowStorageWarning,
          backgroundColor: Colors.orange.shade100,
          onDismiss: () {},
        ),
      ],
    );
  }
}

User Profile Layouts

Flexible User Info Row

class LocalizedUserInfoRow extends StatelessWidget {
  final String avatarUrl;
  final String name;
  final String subtitle;
  final Widget? trailing;

  const LocalizedUserInfoRow({
    super.key,
    required this.avatarUrl,
    required this.name,
    required this.subtitle,
    this.trailing,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        CircleAvatar(
          radius: 24,
          backgroundImage: NetworkImage(avatarUrl),
        ),
        const SizedBox(width: 12),
        Flexible(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                name,
                style: Theme.of(context).textTheme.titleSmall?.copyWith(
                  fontWeight: FontWeight.w600,
                ),
                overflow: TextOverflow.ellipsis,
              ),
              Text(
                subtitle,
                style: Theme.of(context).textTheme.bodySmall?.copyWith(
                  color: Theme.of(context).colorScheme.outline,
                ),
                overflow: TextOverflow.ellipsis,
              ),
            ],
          ),
        ),
        if (trailing != null) ...[
          const SizedBox(width: 12),
          trailing!,
        ],
      ],
    );
  }
}

// Usage
class UserListExample extends StatelessWidget {
  const UserListExample({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        LocalizedUserInfoRow(
          avatarUrl: 'https://i.pravatar.cc/150?img=1',
          name: l10n.userName1,
          subtitle: l10n.userRole1,
          trailing: FilledButton.tonal(
            onPressed: () {},
            child: Text(l10n.followButton),
          ),
        ),
        const Divider(height: 32),
        LocalizedUserInfoRow(
          avatarUrl: 'https://i.pravatar.cc/150?img=2',
          name: l10n.userName2,
          subtitle: l10n.userRole2,
          trailing: OutlinedButton(
            onPressed: () {},
            child: Text(l10n.messageButton),
          ),
        ),
      ],
    );
  }
}

FlexFit Options

Comparing FlexFit.tight vs FlexFit.loose

class FlexFitComparison extends StatelessWidget {
  const FlexFitComparison({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Text(
          l10n.flexFitLooseLabel,
          style: Theme.of(context).textTheme.labelMedium,
        ),
        const SizedBox(height: 8),
        Container(
          color: Colors.grey.shade200,
          padding: const EdgeInsets.all(8),
          child: Row(
            children: [
              Flexible(
                fit: FlexFit.loose, // Default - doesn't force expansion
                child: Container(
                  color: Colors.blue.shade200,
                  padding: const EdgeInsets.all(8),
                  child: Text(l10n.content),
                ),
              ),
              const SizedBox(width: 8),
              Container(
                color: Colors.green.shade200,
                padding: const EdgeInsets.all(8),
                child: const Text('Fixed'),
              ),
            ],
          ),
        ),
        const SizedBox(height: 24),
        Text(
          l10n.flexFitTightLabel,
          style: Theme.of(context).textTheme.labelMedium,
        ),
        const SizedBox(height: 8),
        Container(
          color: Colors.grey.shade200,
          padding: const EdgeInsets.all(8),
          child: Row(
            children: [
              Flexible(
                fit: FlexFit.tight, // Same as Expanded - forces expansion
                child: Container(
                  color: Colors.blue.shade200,
                  padding: const EdgeInsets.all(8),
                  child: Text(l10n.content),
                ),
              ),
              const SizedBox(width: 8),
              Container(
                color: Colors.green.shade200,
                padding: const EdgeInsets.all(8),
                child: const Text('Fixed'),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

Language-Adaptive Flex

Adaptive Flex Based on Language

class AdaptiveFlexLayout extends StatelessWidget {
  final Widget child;
  final int baseFlex;

  const AdaptiveFlexLayout({
    super.key,
    required this.child,
    this.baseFlex = 1,
  });

  @override
  Widget build(BuildContext context) {
    final locale = Localizations.localeOf(context);
    final adjustedFlex = _getAdjustedFlex(locale);

    return Flexible(
      flex: adjustedFlex,
      child: child,
    );
  }

  int _getAdjustedFlex(Locale locale) {
    switch (locale.languageCode) {
      case 'de': // German typically needs more space
      case 'ru': // Russian
      case 'fi': // Finnish
        return baseFlex + 1;
      case 'ja': // Japanese is more compact
      case 'zh': // Chinese
      case 'ko': // Korean
        return baseFlex;
      default:
        return baseFlex;
    }
  }
}

ARB File Structure

English (app_en.arb)

{
  "@@locale": "en",

  "shortMessage": "Quick update available",
  "actionButton": "Update",

  "flexibleLabel": "Flexible (uses natural size)",
  "expandedLabel": "Expanded (fills available space)",
  "shortText": "Short",
  "fixedContent": "Fixed",

  "moreItems": "+{count} more",
  "@moreItems": {
    "placeholders": {
      "count": {"type": "int"}
    }
  },

  "filterAll": "All",
  "filterActive": "Active",
  "filterCompleted": "Completed",
  "filterPending": "Pending",

  "updateAvailableMessage": "A new version is available. Update now to get the latest features.",
  "updateNow": "Update",
  "lowStorageWarning": "Your device is running low on storage space.",

  "userName1": "Sarah Johnson",
  "userRole1": "Product Designer",
  "userName2": "Michael Chen",
  "userRole2": "Software Engineer",
  "followButton": "Follow",
  "messageButton": "Message",

  "flexFitLooseLabel": "FlexFit.loose (default)",
  "flexFitTightLabel": "FlexFit.tight (like Expanded)",
  "content": "Content"
}

German (app_de.arb)

{
  "@@locale": "de",

  "shortMessage": "Schnelles Update verfügbar",
  "actionButton": "Aktualisieren",

  "flexibleLabel": "Flexible (verwendet natürliche Größe)",
  "expandedLabel": "Expanded (füllt verfügbaren Platz)",
  "shortText": "Kurz",
  "fixedContent": "Fest",

  "moreItems": "+{count} weitere",

  "filterAll": "Alle",
  "filterActive": "Aktiv",
  "filterCompleted": "Abgeschlossen",
  "filterPending": "Ausstehend",

  "updateAvailableMessage": "Eine neue Version ist verfügbar. Aktualisieren Sie jetzt, um die neuesten Funktionen zu erhalten.",
  "updateNow": "Aktualisieren",
  "lowStorageWarning": "Auf Ihrem Gerät ist der Speicherplatz knapp.",

  "userName1": "Sarah Johnson",
  "userRole1": "Produktdesignerin",
  "userName2": "Michael Chen",
  "userRole2": "Softwareentwickler",
  "followButton": "Folgen",
  "messageButton": "Nachricht",

  "flexFitLooseLabel": "FlexFit.loose (Standard)",
  "flexFitTightLabel": "FlexFit.tight (wie Expanded)",
  "content": "Inhalt"
}

Arabic (app_ar.arb)

{
  "@@locale": "ar",

  "shortMessage": "تحديث سريع متاح",
  "actionButton": "تحديث",

  "flexibleLabel": "مرن (يستخدم الحجم الطبيعي)",
  "expandedLabel": "موسع (يملأ المساحة المتاحة)",
  "shortText": "قصير",
  "fixedContent": "ثابت",

  "moreItems": "+{count} المزيد",

  "filterAll": "الكل",
  "filterActive": "نشط",
  "filterCompleted": "مكتمل",
  "filterPending": "معلق",

  "updateAvailableMessage": "يتوفر إصدار جديد. قم بالتحديث الآن للحصول على أحدث الميزات.",
  "updateNow": "تحديث",
  "lowStorageWarning": "مساحة التخزين على جهازك تنفد.",

  "userName1": "سارة جونسون",
  "userRole1": "مصممة منتجات",
  "userName2": "مايكل تشين",
  "userRole2": "مهندس برمجيات",
  "followButton": "متابعة",
  "messageButton": "رسالة",

  "flexFitLooseLabel": "FlexFit.loose (افتراضي)",
  "flexFitTightLabel": "FlexFit.tight (مثل Expanded)",
  "content": "محتوى"
}

Best Practices Summary

Do's

  1. Use Flexible when content size varies and shouldn't force expansion
  2. Choose FlexFit.loose for content that should use natural size
  3. Combine with overflow handling (TextOverflow.ellipsis)
  4. Use flex values to control proportional space allocation
  5. Test with varying content lengths across languages

Don'ts

  1. Don't confuse with Expanded when you want natural sizing
  2. Don't forget overflow handling for text content
  3. Don't use in scrollable containers without consideration
  4. Don't ignore RTL testing for flexible layouts

Accessibility Considerations

class AccessibleFlexibleContent extends StatelessWidget {
  final String content;
  final String semanticHint;

  const AccessibleFlexibleContent({
    super.key,
    required this.content,
    required this.semanticHint,
  });

  @override
  Widget build(BuildContext context) {
    return Semantics(
      hint: semanticHint,
      child: Flexible(
        child: Text(
          content,
          overflow: TextOverflow.ellipsis,
        ),
      ),
    );
  }
}

Conclusion

Flexible provides fine-grained control over space allocation in multilingual Flutter applications. Unlike Expanded, it allows content to remain at its natural size while still participating in flex layout. This makes it ideal for situations where content length varies across languages but shouldn't necessarily fill all available space. Use Flexible strategically alongside Expanded to create layouts that adapt gracefully to different content lengths.

Further Reading