← Back to Blog

Flutter Localization with State Management: Provider, Riverpod, and Bloc Patterns

flutterlocalizationstate-managementproviderriverpodbloc

Flutter Localization with State Management: Provider, Riverpod, and Bloc Patterns

Building a language switcher in Flutter? You need state management that works seamlessly with localization. This guide shows how to implement runtime language switching using Provider, Riverpod, and Bloc.

Why State Management for Localization?

Flutter's built-in localization follows the system locale by default. But users often want to:

  • Override system language
  • Switch languages instantly
  • Persist language preference
  • Support languages their system doesn't have

State management solves these requirements elegantly.

The Basic Pattern

All approaches follow this pattern:

User selects language → State updates → MaterialApp rebuilds → New translations load

The key is making MaterialApp reactive to locale changes:

MaterialApp(
  locale: currentLocale,  // This must be reactive
  supportedLocales: [...],
  localizationsDelegates: [...],
)

Provider Implementation

Provider is the simplest approach for most apps.

Step 1: Create LocaleProvider

// lib/providers/locale_provider.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class LocaleProvider extends ChangeNotifier {
  Locale? _locale;

  Locale? get locale => _locale;

  // Initialize from saved preference
  Future<void> init() async {
    final prefs = await SharedPreferences.getInstance();
    final languageCode = prefs.getString('language_code');
    if (languageCode != null) {
      _locale = Locale(languageCode);
      notifyListeners();
    }
  }

  // Change locale and persist
  Future<void> setLocale(Locale locale) async {
    _locale = locale;
    notifyListeners();

    // Persist preference
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('language_code', locale.languageCode);
  }

  // Clear preference and use system locale
  Future<void> clearLocale() async {
    _locale = null;
    notifyListeners();

    final prefs = await SharedPreferences.getInstance();
    await prefs.remove('language_code');
  }
}

Step 2: Set Up Provider

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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final localeProvider = LocaleProvider();
  await localeProvider.init();

  runApp(
    ChangeNotifierProvider.value(
      value: localeProvider,
      child: const MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return Consumer<LocaleProvider>(
      builder: (context, localeProvider, child) {
        return MaterialApp(
          locale: localeProvider.locale,
          supportedLocales: AppLocalizations.supportedLocales,
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          home: const HomePage(),
        );
      },
    );
  }
}

Step 3: Create Language Switcher Widget

// lib/widgets/language_switcher.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

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

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

    return PopupMenuButton<Locale>(
      icon: const Icon(Icons.language),
      tooltip: l10n.changeLanguage,
      onSelected: (Locale locale) {
        context.read<LocaleProvider>().setLocale(locale);
      },
      itemBuilder: (context) => [
        _buildMenuItem(context, const Locale('en'), 'English', currentLocale),
        _buildMenuItem(context, const Locale('es'), 'Español', currentLocale),
        _buildMenuItem(context, const Locale('fr'), 'Français', currentLocale),
        _buildMenuItem(context, const Locale('de'), 'Deutsch', currentLocale),
        _buildMenuItem(context, const Locale('ar'), 'العربية', currentLocale),
      ],
    );
  }

  PopupMenuItem<Locale> _buildMenuItem(
    BuildContext context,
    Locale locale,
    String name,
    Locale currentLocale,
  ) {
    final isSelected = locale.languageCode == currentLocale.languageCode;

    return PopupMenuItem<Locale>(
      value: locale,
      child: Row(
        children: [
          if (isSelected)
            const Icon(Icons.check, size: 18)
          else
            const SizedBox(width: 18),
          const SizedBox(width: 8),
          Text(name),
        ],
      ),
    );
  }
}

Usage

AppBar(
  title: Text(AppLocalizations.of(context)!.appTitle),
  actions: const [
    LanguageSwitcher(),
  ],
)

Riverpod Implementation

Riverpod offers better testability and compile-time safety.

Step 1: Create Locale Notifier

// lib/providers/locale_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

// Shared preferences provider
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
  throw UnimplementedError('Initialize in main');
});

// Locale state notifier
class LocaleNotifier extends StateNotifier<Locale?> {
  final SharedPreferences prefs;

  LocaleNotifier(this.prefs) : super(null) {
    _loadSavedLocale();
  }

  void _loadSavedLocale() {
    final languageCode = prefs.getString('language_code');
    if (languageCode != null) {
      state = Locale(languageCode);
    }
  }

  Future<void> setLocale(Locale locale) async {
    state = locale;
    await prefs.setString('language_code', locale.languageCode);
  }

  Future<void> clearLocale() async {
    state = null;
    await prefs.remove('language_code');
  }
}

// Provider
final localeProvider = StateNotifierProvider<LocaleNotifier, Locale?>((ref) {
  final prefs = ref.watch(sharedPreferencesProvider);
  return LocaleNotifier(prefs);
});

Step 2: Set Up Riverpod

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final prefs = await SharedPreferences.getInstance();

  runApp(
    ProviderScope(
      overrides: [
        sharedPreferencesProvider.overrideWithValue(prefs),
      ],
      child: const MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final locale = ref.watch(localeProvider);

    return MaterialApp(
      locale: locale,
      supportedLocales: AppLocalizations.supportedLocales,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      home: const HomePage(),
    );
  }
}

Step 3: Language Switcher with Riverpod

// lib/widgets/language_switcher.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class LanguageSwitcher extends ConsumerWidget {
  const LanguageSwitcher({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final l10n = AppLocalizations.of(context)!;
    final currentLocale = Localizations.localeOf(context);

    return DropdownButton<Locale>(
      value: currentLocale,
      underline: const SizedBox(),
      icon: const Icon(Icons.language),
      items: const [
        DropdownMenuItem(value: Locale('en'), child: Text('English')),
        DropdownMenuItem(value: Locale('es'), child: Text('Español')),
        DropdownMenuItem(value: Locale('fr'), child: Text('Français')),
        DropdownMenuItem(value: Locale('de'), child: Text('Deutsch')),
      ],
      onChanged: (Locale? locale) {
        if (locale != null) {
          ref.read(localeProvider.notifier).setLocale(locale);
        }
      },
    );
  }
}

Riverpod Code Generation (Recommended)

With riverpod_generator:

// lib/providers/locale_provider.dart
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';

part 'locale_provider.g.dart';

@riverpod
class Locale extends _$Locale {
  @override
  Locale? build() {
    _loadSavedLocale();
    return null;
  }

  Future<void> _loadSavedLocale() async {
    final prefs = await SharedPreferences.getInstance();
    final languageCode = prefs.getString('language_code');
    if (languageCode != null) {
      state = Locale(languageCode);
    }
  }

  Future<void> setLocale(Locale locale) async {
    state = locale;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('language_code', locale.languageCode);
  }
}

Bloc Implementation

Bloc is ideal for complex apps with strict architectural requirements.

Step 1: Define Events and States

// lib/bloc/locale/locale_event.dart
import 'package:flutter/material.dart';

abstract class LocaleEvent {}

class LocaleChanged extends LocaleEvent {
  final Locale locale;
  LocaleChanged(this.locale);
}

class LocaleCleared extends LocaleEvent {}

class LocaleLoaded extends LocaleEvent {}
// lib/bloc/locale/locale_state.dart
import 'package:flutter/material.dart';

class LocaleState {
  final Locale? locale;
  final bool isLoading;

  const LocaleState({this.locale, this.isLoading = false});

  LocaleState copyWith({Locale? locale, bool? isLoading}) {
    return LocaleState(
      locale: locale ?? this.locale,
      isLoading: isLoading ?? this.isLoading,
    );
  }
}

Step 2: Create Bloc

// lib/bloc/locale/locale_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'locale_event.dart';
import 'locale_state.dart';

class LocaleBloc extends Bloc<LocaleEvent, LocaleState> {
  final SharedPreferences prefs;

  LocaleBloc(this.prefs) : super(const LocaleState(isLoading: true)) {
    on<LocaleLoaded>(_onLocaleLoaded);
    on<LocaleChanged>(_onLocaleChanged);
    on<LocaleCleared>(_onLocaleCleared);

    // Load saved locale on initialization
    add(LocaleLoaded());
  }

  Future<void> _onLocaleLoaded(
    LocaleLoaded event,
    Emitter<LocaleState> emit,
  ) async {
    final languageCode = prefs.getString('language_code');
    final locale = languageCode != null ? Locale(languageCode) : null;
    emit(LocaleState(locale: locale, isLoading: false));
  }

  Future<void> _onLocaleChanged(
    LocaleChanged event,
    Emitter<LocaleState> emit,
  ) async {
    await prefs.setString('language_code', event.locale.languageCode);
    emit(state.copyWith(locale: event.locale));
  }

  Future<void> _onLocaleCleared(
    LocaleCleared event,
    Emitter<LocaleState> emit,
  ) async {
    await prefs.remove('language_code');
    emit(const LocaleState(locale: null));
  }
}

Step 3: Set Up Bloc Provider

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final prefs = await SharedPreferences.getInstance();

  runApp(
    BlocProvider(
      create: (context) => LocaleBloc(prefs),
      child: const MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LocaleBloc, LocaleState>(
      builder: (context, state) {
        if (state.isLoading) {
          return const MaterialApp(
            home: Scaffold(
              body: Center(child: CircularProgressIndicator()),
            ),
          );
        }

        return MaterialApp(
          locale: state.locale,
          supportedLocales: AppLocalizations.supportedLocales,
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          home: const HomePage(),
        );
      },
    );
  }
}

Step 4: Language Switcher with Bloc

// lib/widgets/language_switcher.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

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

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

    return BlocBuilder<LocaleBloc, LocaleState>(
      builder: (context, state) {
        return ListTile(
          leading: const Icon(Icons.language),
          title: Text(l10n.language),
          subtitle: Text(_getLanguageName(state.locale)),
          onTap: () => _showLanguagePicker(context),
        );
      },
    );
  }

  String _getLanguageName(Locale? locale) {
    switch (locale?.languageCode) {
      case 'en': return 'English';
      case 'es': return 'Español';
      case 'fr': return 'Français';
      case 'de': return 'Deutsch';
      default: return 'System Default';
    }
  }

  void _showLanguagePicker(BuildContext context) {
    showModalBottomSheet(
      context: context,
      builder: (context) => Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            title: const Text('System Default'),
            onTap: () {
              context.read<LocaleBloc>().add(LocaleCleared());
              Navigator.pop(context);
            },
          ),
          ListTile(
            title: const Text('English'),
            onTap: () {
              context.read<LocaleBloc>().add(LocaleChanged(const Locale('en')));
              Navigator.pop(context);
            },
          ),
          ListTile(
            title: const Text('Español'),
            onTap: () {
              context.read<LocaleBloc>().add(LocaleChanged(const Locale('es')));
              Navigator.pop(context);
            },
          ),
          // Add more languages...
        ],
      ),
    );
  }
}

Comparison: Which to Choose?

Feature Provider Riverpod Bloc
Setup complexity Low Medium High
Boilerplate Low Low High
Testability Good Excellent Excellent
Type safety Good Excellent Good
Learning curve Easy Medium Steep
Best for Simple apps Most apps Complex apps

Recommendations:

  • Provider: Quick prototypes, simple apps, teams new to Flutter
  • Riverpod: Production apps, better architecture, most use cases
  • Bloc: Large teams, strict patterns, complex business logic

Advanced Patterns

Locale with Region Support

class LocaleProvider extends ChangeNotifier {
  Locale? _locale;

  void setLocale(String languageCode, [String? countryCode]) {
    _locale = countryCode != null
        ? Locale(languageCode, countryCode)
        : Locale(languageCode);
    notifyListeners();
  }
}

// Usage: Different Spanish for Spain vs Mexico
localeProvider.setLocale('es', 'ES');  // Spanish (Spain)
localeProvider.setLocale('es', 'MX');  // Spanish (Mexico)

Locale Resolution Callback

MaterialApp(
  locale: localeProvider.locale,
  localeResolutionCallback: (deviceLocale, supportedLocales) {
    // Custom resolution logic
    if (localeProvider.locale != null) {
      return localeProvider.locale;
    }

    // Check if device locale is supported
    for (var locale in supportedLocales) {
      if (locale.languageCode == deviceLocale?.languageCode) {
        return locale;
      }
    }

    // Default to English
    return const Locale('en');
  },
)

Combining with Theme

class AppSettingsProvider extends ChangeNotifier {
  Locale? _locale;
  ThemeMode _themeMode = ThemeMode.system;

  Locale? get locale => _locale;
  ThemeMode get themeMode => _themeMode;

  void setLocale(Locale locale) {
    _locale = locale;
    notifyListeners();
  }

  void setThemeMode(ThemeMode mode) {
    _themeMode = mode;
    notifyListeners();
  }
}

// Usage in MaterialApp
Consumer<AppSettingsProvider>(
  builder: (context, settings, child) {
    return MaterialApp(
      locale: settings.locale,
      themeMode: settings.themeMode,
      // ...
    );
  },
)

Testing Localization State

Provider Tests

void main() {
  group('LocaleProvider', () {
    late LocaleProvider provider;
    late SharedPreferences prefs;

    setUp(() async {
      SharedPreferences.setMockInitialValues({});
      prefs = await SharedPreferences.getInstance();
      provider = LocaleProvider();
    });

    test('setLocale updates state and persists', () async {
      await provider.setLocale(const Locale('es'));

      expect(provider.locale?.languageCode, 'es');
      expect(prefs.getString('language_code'), 'es');
    });

    test('init loads saved locale', () async {
      await prefs.setString('language_code', 'fr');
      await provider.init();

      expect(provider.locale?.languageCode, 'fr');
    });
  });
}

Widget Tests

testWidgets('language switcher changes locale', (tester) async {
  final provider = LocaleProvider();

  await tester.pumpWidget(
    ChangeNotifierProvider.value(
      value: provider,
      child: MaterialApp(
        localizationsDelegates: AppLocalizations.localizationsDelegates,
        supportedLocales: AppLocalizations.supportedLocales,
        home: const LanguageSwitcher(),
      ),
    ),
  );

  // Open language menu
  await tester.tap(find.byIcon(Icons.language));
  await tester.pumpAndSettle();

  // Select Spanish
  await tester.tap(find.text('Español'));
  await tester.pumpAndSettle();

  expect(provider.locale?.languageCode, 'es');
});

Summary

State management is essential for runtime language switching in Flutter:

  • Provider: Simplest setup, great for most apps
  • Riverpod: Better testability and type safety
  • Bloc: Strict patterns for complex apps

Key principles:

  1. Store locale in state management
  2. Persist preference with SharedPreferences
  3. Make MaterialApp reactive to locale changes
  4. Provide clear UI for language selection
  5. Test thoroughly

Choose based on your app's complexity and team familiarity.


Managing translations for multiple languages? FlutterLocalisation makes it easy with AI translation and team collaboration. Start for free.