Flutter Provider Localization: Simple State Management for Multi-Language Apps
Provider is Flutter's recommended simple state management solution. This guide shows you how to implement language switching with Provider - no complex setup required.
Why Use Provider for Localization?
Provider is perfect for localization because:
- Simple API - Easy to understand and implement
- Lightweight - Minimal boilerplate code
- Built-in Flutter support - Recommended by the Flutter team
- Scoped access - Access locale anywhere in the widget tree
Project Setup
1. Add Dependencies
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
provider: ^6.1.1
shared_preferences: ^2.2.2
intl: ^0.18.1
2. Configure l10n.yaml
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
3. Create ARB Files
lib/l10n/app_en.arb:
{
"@@locale": "en",
"appTitle": "My App",
"welcome": "Welcome",
"settings": "Settings",
"language": "Language",
"selectLanguage": "Select Language",
"english": "English",
"spanish": "Spanish",
"french": "French",
"german": "German",
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@itemCount": {
"placeholders": {
"count": {"type": "int"}
}
}
}
lib/l10n/app_es.arb:
{
"@@locale": "es",
"appTitle": "Mi Aplicación",
"welcome": "Bienvenido",
"settings": "Configuración",
"language": "Idioma",
"selectLanguage": "Seleccionar Idioma",
"english": "Inglés",
"spanish": "Español",
"french": "Francés",
"german": "Alemán",
"itemCount": "{count, plural, =0{Sin artículos} =1{1 artículo} other{{count} artículos}}"
}
Creating the Locale Provider
Basic LocaleProvider
// lib/providers/locale_provider.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LocaleProvider extends ChangeNotifier {
static const String _localeKey = 'selected_locale';
Locale _locale = const Locale('en');
bool _isLoading = true;
Locale get locale => _locale;
bool get isLoading => _isLoading;
// Supported locales
static const List<Locale> supportedLocales = [
Locale('en'),
Locale('es'),
Locale('fr'),
Locale('de'),
];
LocaleProvider() {
_loadSavedLocale();
}
Future<void> _loadSavedLocale() async {
try {
final prefs = await SharedPreferences.getInstance();
final languageCode = prefs.getString(_localeKey);
if (languageCode != null) {
_locale = Locale(languageCode);
}
} catch (e) {
debugPrint('Error loading locale: $e');
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> setLocale(Locale locale) async {
if (!supportedLocales.contains(locale)) return;
if (_locale == locale) return;
_locale = locale;
notifyListeners();
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_localeKey, locale.languageCode);
} catch (e) {
debugPrint('Error saving locale: $e');
}
}
void clearLocale() async {
_locale = const Locale('en');
notifyListeners();
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_localeKey);
}
}
Provider with Country Code Support
// lib/providers/locale_provider.dart
class LocaleProvider extends ChangeNotifier {
static const String _languageKey = 'language_code';
static const String _countryKey = 'country_code';
Locale _locale = const Locale('en', 'US');
Locale get locale => _locale;
static const List<Locale> supportedLocales = [
Locale('en', 'US'),
Locale('en', 'GB'),
Locale('es', 'ES'),
Locale('es', 'MX'),
Locale('fr', 'FR'),
Locale('de', 'DE'),
Locale('pt', 'BR'),
Locale('zh', 'CN'),
];
LocaleProvider() {
_loadSavedLocale();
}
Future<void> _loadSavedLocale() async {
final prefs = await SharedPreferences.getInstance();
final languageCode = prefs.getString(_languageKey);
final countryCode = prefs.getString(_countryKey);
if (languageCode != null) {
_locale = Locale(languageCode, countryCode);
notifyListeners();
}
}
Future<void> setLocale(Locale locale) async {
if (_locale == locale) return;
_locale = locale;
notifyListeners();
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_languageKey, locale.languageCode);
if (locale.countryCode != null) {
await prefs.setString(_countryKey, locale.countryCode!);
} else {
await prefs.remove(_countryKey);
}
}
}
Setting Up the App
Main App Configuration
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'providers/locale_provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => LocaleProvider(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return Consumer<LocaleProvider>(
builder: (context, localeProvider, _) {
// Show loading while locale is being loaded
if (localeProvider.isLoading) {
return const MaterialApp(
home: Scaffold(
body: Center(child: CircularProgressIndicator()),
),
);
}
return MaterialApp(
title: 'Flutter Provider Localization',
// Localization configuration
locale: localeProvider.locale,
supportedLocales: LocaleProvider.supportedLocales,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: const HomePage(),
);
},
);
}
}
Using Multiple Providers
// lib/main.dart
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => LocaleProvider()),
ChangeNotifierProvider(create: (_) => ThemeProvider()),
ChangeNotifierProvider(create: (_) => AuthProvider()),
],
child: const MyApp(),
),
);
}
Accessing Translations
In Widgets
// lib/pages/home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.appTitle),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.welcome,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 16),
Text(l10n.itemCount(5)),
],
),
),
);
}
}
Extension for Cleaner Access
// lib/extensions/context_extensions.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import '../providers/locale_provider.dart';
extension LocalizationExtension on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this)!;
LocaleProvider get localeProvider => read<LocaleProvider>();
Locale get currentLocale => watch<LocaleProvider>().locale;
void setLocale(Locale locale) {
read<LocaleProvider>().setLocale(locale);
}
}
// Usage:
Text(context.l10n.welcome)
context.setLocale(const Locale('es'));
Language Selector Widget
Dropdown Selector
// lib/widgets/language_dropdown.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../providers/locale_provider.dart';
class LanguageDropdown extends StatelessWidget {
const LanguageDropdown({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Consumer<LocaleProvider>(
builder: (context, provider, _) {
return DropdownButton<Locale>(
value: provider.locale,
onChanged: (Locale? newLocale) {
if (newLocale != null) {
provider.setLocale(newLocale);
}
},
items: LocaleProvider.supportedLocales.map((locale) {
return DropdownMenuItem<Locale>(
value: locale,
child: Text(_getLanguageName(locale, l10n)),
);
}).toList(),
);
},
);
}
String _getLanguageName(Locale locale, AppLocalizations l10n) {
switch (locale.languageCode) {
case 'en':
return l10n.english;
case 'es':
return l10n.spanish;
case 'fr':
return l10n.french;
case 'de':
return l10n.german;
default:
return locale.languageCode;
}
}
}
ListTile Selector
// lib/widgets/language_list_tile.dart
class LanguageListTile extends StatelessWidget {
const LanguageListTile({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Consumer<LocaleProvider>(
builder: (context, provider, _) {
return ListTile(
leading: const Icon(Icons.language),
title: Text(l10n.language),
subtitle: Text(_getLanguageName(provider.locale)),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showLanguageDialog(context, provider),
);
},
);
}
void _showLanguageDialog(BuildContext context, LocaleProvider provider) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context)!.selectLanguage),
content: Column(
mainAxisSize: MainAxisSize.min,
children: LocaleProvider.supportedLocales.map((locale) {
final isSelected = locale == provider.locale;
return ListTile(
title: Text(_getLanguageName(locale)),
trailing: isSelected ? const Icon(Icons.check) : null,
onTap: () {
provider.setLocale(locale);
Navigator.pop(context);
},
);
}).toList(),
),
),
);
}
String _getLanguageName(Locale locale) {
const names = {
'en': 'English',
'es': 'Español',
'fr': 'Français',
'de': 'Deutsch',
};
return names[locale.languageCode] ?? locale.languageCode;
}
}
Settings Page Example
// lib/pages/settings_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../providers/locale_provider.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.settings),
),
body: ListView(
children: [
// Language Section
const _SectionHeader(title: 'Language'),
Consumer<LocaleProvider>(
builder: (context, provider, _) {
return Column(
children: LocaleProvider.supportedLocales.map((locale) {
final isSelected = locale == provider.locale;
return RadioListTile<Locale>(
title: Text(_getLanguageName(locale)),
subtitle: Text(_getNativeName(locale)),
value: locale,
groupValue: provider.locale,
onChanged: (value) {
if (value != null) {
provider.setLocale(value);
}
},
secondary: isSelected
? const Icon(Icons.check, color: Colors.green)
: null,
);
}).toList(),
);
},
),
],
),
);
}
String _getLanguageName(Locale locale) {
const names = {
'en': 'English',
'es': 'Spanish',
'fr': 'French',
'de': 'German',
};
return names[locale.languageCode] ?? locale.languageCode;
}
String _getNativeName(Locale locale) {
const names = {
'en': 'English',
'es': 'Español',
'fr': 'Français',
'de': 'Deutsch',
};
return names[locale.languageCode] ?? locale.languageCode;
}
}
class _SectionHeader extends StatelessWidget {
final String title;
const _SectionHeader({required this.title});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary,
),
),
);
}
}
System Locale Detection
// lib/providers/locale_provider.dart
class LocaleProvider extends ChangeNotifier {
// ...
Future<void> _loadSavedLocale() async {
final prefs = await SharedPreferences.getInstance();
final savedLanguageCode = prefs.getString(_localeKey);
if (savedLanguageCode != null) {
// Use saved preference
_locale = Locale(savedLanguageCode);
} else {
// Fall back to system locale
_locale = _getSystemLocale();
}
_isLoading = false;
notifyListeners();
}
Locale _getSystemLocale() {
final systemLocale = WidgetsBinding.instance.platformDispatcher.locale;
// Check if system locale is supported
for (final supported in supportedLocales) {
if (supported.languageCode == systemLocale.languageCode) {
return supported;
}
}
// Default to English
return const Locale('en');
}
// Follow system locale
Future<void> useSystemLocale() async {
_locale = _getSystemLocale();
notifyListeners();
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_localeKey);
}
}
Testing with Provider
// test/locale_provider_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/providers/locale_provider.dart';
void main() {
group('LocaleProvider', () {
test('initial locale is English', () {
final provider = LocaleProvider();
expect(provider.locale.languageCode, 'en');
});
test('setLocale changes the locale', () async {
final provider = LocaleProvider();
await provider.setLocale(const Locale('es'));
expect(provider.locale.languageCode, 'es');
});
test('notifies listeners on locale change', () async {
final provider = LocaleProvider();
var notified = false;
provider.addListener(() => notified = true);
await provider.setLocale(const Locale('fr'));
expect(notified, true);
});
});
}
Widget Testing
// test/widgets/language_dropdown_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
testWidgets('LanguageDropdown changes locale', (tester) async {
final provider = LocaleProvider();
await tester.pumpWidget(
ChangeNotifierProvider.value(
value: provider,
child: Consumer<LocaleProvider>(
builder: (context, provider, _) {
return MaterialApp(
locale: provider.locale,
supportedLocales: LocaleProvider.supportedLocales,
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
home: Scaffold(body: LanguageDropdown()),
);
},
),
),
);
// Open dropdown
await tester.tap(find.byType(DropdownButton<Locale>));
await tester.pumpAndSettle();
// Select Spanish
await tester.tap(find.text('Español').last);
await tester.pumpAndSettle();
expect(provider.locale.languageCode, 'es');
});
}
Best Practices
1. Initialize Early
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Pre-load locale before app starts
final localeProvider = LocaleProvider();
await localeProvider.initialize();
runApp(
ChangeNotifierProvider.value(
value: localeProvider,
child: const MyApp(),
),
);
}
2. Handle Loading State
Consumer<LocaleProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const SplashScreen();
}
return child!;
},
child: const MainApp(),
)
3. Avoid Rebuilding Entire App
// Good - Only locale-dependent widgets rebuild
Selector<LocaleProvider, Locale>(
selector: (_, provider) => provider.locale,
builder: (context, locale, child) {
return Text(AppLocalizations.of(context)!.welcome);
},
)
Conclusion
Provider makes Flutter localization simple and maintainable. The pattern shown here:
- Persists user language preference
- Falls back to system locale
- Provides clean widget access
- Is easy to test
For more complex apps with many features, consider Riverpod which offers additional features like compile-time safety and better testing support.
Need help managing your ARB translations? FlutterLocalisation provides a complete platform for managing Flutter localization files with team collaboration and AI-powered translations.