← Back to Blog

Flutter MaterialBanner Localization: Persistent Banners for Multilingual Apps

fluttermaterialbannerbannermessageslocalizationrtl

Flutter MaterialBanner Localization: Persistent Banners for Multilingual Apps

MaterialBanner is a Flutter widget that displays a prominent message with optional actions at the top of the screen. In multilingual applications, MaterialBanner is essential for showing translated system messages and warnings that persist until dismissed, providing localized action buttons for user responses, supporting RTL banner layouts with correctly positioned icons and actions, and delivering accessible announcements in the active language.

Understanding MaterialBanner in Localization Context

MaterialBanner renders a Material Design banner that stays visible until the user takes action or dismisses it. For multilingual apps, this enables:

  • Translated warning and info messages that persist across scroll
  • Localized action buttons like "Dismiss", "Learn more", or "Update now"
  • RTL-aware icon and action button positioning
  • Accessible banner content announced in the active language

Why MaterialBanner Matters for Multilingual Apps

MaterialBanner provides:

  • Persistent messages: Translated banners that stay visible until user action
  • Multiple actions: Up to two localized action buttons per banner
  • Leading widget: Icons or avatars alongside translated message text
  • ScaffoldMessenger integration: Show and hide banners programmatically

Basic MaterialBanner Implementation

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

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.homeTitle)),
      body: Center(
        child: FilledButton(
          onPressed: () {
            ScaffoldMessenger.of(context).showMaterialBanner(
              MaterialBanner(
                content: Text(l10n.updateAvailableMessage),
                leading: const Icon(Icons.system_update),
                actions: [
                  TextButton(
                    onPressed: () {
                      ScaffoldMessenger.of(context)
                          .hideCurrentMaterialBanner();
                    },
                    child: Text(l10n.dismissLabel),
                  ),
                  TextButton(
                    onPressed: () {
                      ScaffoldMessenger.of(context)
                          .hideCurrentMaterialBanner();
                    },
                    child: Text(l10n.updateNowLabel),
                  ),
                ],
              ),
            );
          },
          child: Text(l10n.checkForUpdatesLabel),
        ),
      ),
    );
  }
}

Advanced MaterialBanner Patterns for Localization

Connectivity Banner with Translated Status

A banner that shows network connectivity status with translated messages.

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

  @override
  State<ConnectivityBanner> createState() => _ConnectivityBannerState();
}

class _ConnectivityBannerState extends State<ConnectivityBanner> {
  bool _isOffline = false;

  void _toggleConnectivity() {
    setState(() => _isOffline = !_isOffline);
    final l10n = AppLocalizations.of(context)!;

    if (_isOffline) {
      ScaffoldMessenger.of(context).showMaterialBanner(
        MaterialBanner(
          content: Text(l10n.offlineMessage),
          leading: const Icon(Icons.wifi_off),
          backgroundColor: Theme.of(context).colorScheme.errorContainer,
          actions: [
            TextButton(
              onPressed: () {
                ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
                setState(() => _isOffline = false);
              },
              child: Text(l10n.retryLabel),
            ),
            TextButton(
              onPressed: () {
                ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
              },
              child: Text(l10n.dismissLabel),
            ),
          ],
        ),
      );
    } else {
      ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
    }
  }

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.appName)),
      body: Center(
        child: FilledButton(
          onPressed: _toggleConnectivity,
          child: Text(
            _isOffline ? l10n.goOnlineLabel : l10n.simulateOfflineLabel,
          ),
        ),
      ),
    );
  }
}

Permission Request Banner

A banner requesting user permission with translated explanation and action buttons.

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.settingsTitle)),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Card(
            child: ListTile(
              leading: const Icon(Icons.notifications),
              title: Text(l10n.notificationsLabel),
              subtitle: Text(l10n.notificationsDescription),
              trailing: FilledButton.tonal(
                onPressed: () {
                  ScaffoldMessenger.of(context).showMaterialBanner(
                    MaterialBanner(
                      content: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            l10n.notificationPermissionTitle,
                            style: Theme.of(context).textTheme.titleSmall,
                          ),
                          const SizedBox(height: 4),
                          Text(l10n.notificationPermissionMessage),
                        ],
                      ),
                      leading: const Icon(Icons.notifications_active),
                      actions: [
                        TextButton(
                          onPressed: () {
                            ScaffoldMessenger.of(context)
                                .hideCurrentMaterialBanner();
                          },
                          child: Text(l10n.notNowLabel),
                        ),
                        TextButton(
                          onPressed: () {
                            ScaffoldMessenger.of(context)
                                .hideCurrentMaterialBanner();
                          },
                          child: Text(l10n.enableLabel),
                        ),
                      ],
                    ),
                  );
                },
                child: Text(l10n.enableLabel),
              ),
            ),
          ),
          Card(
            child: ListTile(
              leading: const Icon(Icons.location_on),
              title: Text(l10n.locationLabel),
              subtitle: Text(l10n.locationDescription),
              trailing: FilledButton.tonal(
                onPressed: () {
                  ScaffoldMessenger.of(context).showMaterialBanner(
                    MaterialBanner(
                      content: Text(l10n.locationPermissionMessage),
                      leading: const Icon(Icons.location_on),
                      actions: [
                        TextButton(
                          onPressed: () {
                            ScaffoldMessenger.of(context)
                                .hideCurrentMaterialBanner();
                          },
                          child: Text(l10n.denyLabel),
                        ),
                        TextButton(
                          onPressed: () {
                            ScaffoldMessenger.of(context)
                                .hideCurrentMaterialBanner();
                          },
                          child: Text(l10n.allowLabel),
                        ),
                      ],
                    ),
                  );
                },
                child: Text(l10n.enableLabel),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Themed Banners for Different Severity Levels

MaterialBanners styled by severity with translated messages for info, warning, and error states.

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

  void _showBanner(BuildContext context, _BannerType type) {
    final l10n = AppLocalizations.of(context)!;
    final theme = Theme.of(context);

    final (message, icon, bgColor) = switch (type) {
      _BannerType.info => (
          l10n.infoBannerMessage,
          Icons.info_outline,
          theme.colorScheme.primaryContainer,
        ),
      _BannerType.warning => (
          l10n.warningBannerMessage,
          Icons.warning_amber,
          theme.colorScheme.tertiaryContainer,
        ),
      _BannerType.error => (
          l10n.errorBannerMessage,
          Icons.error_outline,
          theme.colorScheme.errorContainer,
        ),
    };

    ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
    ScaffoldMessenger.of(context).showMaterialBanner(
      MaterialBanner(
        content: Text(message),
        leading: Icon(icon),
        backgroundColor: bgColor,
        actions: [
          TextButton(
            onPressed: () {
              ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
            },
            child: Text(l10n.dismissLabel),
          ),
          if (type == _BannerType.error)
            TextButton(
              onPressed: () {
                ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
              },
              child: Text(l10n.viewDetailsLabel),
            ),
        ],
      ),
    );
  }

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.bannersTitle)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            FilledButton.tonal(
              onPressed: () => _showBanner(context, _BannerType.info),
              child: Text(l10n.showInfoBannerLabel),
            ),
            const SizedBox(height: 12),
            FilledButton.tonal(
              onPressed: () => _showBanner(context, _BannerType.warning),
              child: Text(l10n.showWarningBannerLabel),
            ),
            const SizedBox(height: 12),
            FilledButton.tonal(
              onPressed: () => _showBanner(context, _BannerType.error),
              child: Text(l10n.showErrorBannerLabel),
            ),
          ],
        ),
      ),
    );
  }
}

enum _BannerType { info, warning, error }

RTL Support and Bidirectional Layouts

MaterialBanner automatically adapts to RTL layouts. The leading icon moves to the right side, and action buttons align correctly based on text direction.

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.appName)),
      body: Center(
        child: FilledButton(
          onPressed: () {
            ScaffoldMessenger.of(context).showMaterialBanner(
              MaterialBanner(
                padding: const EdgeInsetsDirectional.all(16),
                content: Text(l10n.trialExpiringMessage),
                leading: const Icon(Icons.timer),
                actions: [
                  TextButton(
                    onPressed: () {
                      ScaffoldMessenger.of(context)
                          .hideCurrentMaterialBanner();
                    },
                    child: Text(l10n.remindLaterLabel),
                  ),
                  TextButton(
                    onPressed: () {
                      ScaffoldMessenger.of(context)
                          .hideCurrentMaterialBanner();
                    },
                    child: Text(l10n.upgradeNowLabel),
                  ),
                ],
              ),
            );
          },
          child: Text(l10n.showBannerLabel),
        ),
      ),
    );
  }
}

Testing MaterialBanner 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 buildTestWidget({Locale locale = const Locale('en')}) {
    return MaterialApp(
      locale: locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const LocalizedMaterialBannerExample(),
    );
  }

  testWidgets('MaterialBanner shows localized content', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    await tester.tap(find.byType(FilledButton));
    await tester.pumpAndSettle();
    expect(find.byType(MaterialBanner), findsOneWidget);
  });

  testWidgets('MaterialBanner works in RTL', (tester) async {
    await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
    await tester.pumpAndSettle();
    expect(tester.takeException(), isNull);
  });
}

Best Practices

  1. Always provide a dismiss action with a translated label so users can close the banner in any language.

  2. Use ScaffoldMessenger.hideCurrentMaterialBanner() before showing a new one to prevent stacking multiple translated banners.

  3. Style banners by severity using backgroundColor with semantic colors from the theme's color scheme for info, warning, and error states.

  4. Keep banner messages concise since space is limited — use the action buttons for detailed follow-up in the user's language.

  5. Use EdgeInsetsDirectional for custom padding so the banner adapts correctly in RTL layouts.

  6. Test banner dismissal in RTL to verify action buttons align correctly and the leading icon appears on the correct side.

Conclusion

MaterialBanner provides a persistent message bar for Flutter apps that stays visible until user action. For multilingual apps, it handles translated messages with localized action buttons, supports severity-based styling, and automatically adapts to RTL layouts. By combining MaterialBanner with connectivity alerts, permission requests, and themed severity levels, you can deliver important messages that communicate clearly in every supported language.

Further Reading