Mapping Flutter Localizations Without Context
How about not writing the localization mapping by hand, hat-trick!
This a complete example of how to access localizations in models and during testing without the context. Neither the Flutter SDK docs nor the Code Labs docs provide one. And so this is a free post providing a full example for reference.
Why Do We Need Context
In order to develop techniques for accessing localizations without context, we have to know why context is being used. In the localization case, context is being used so that we get the correct locale text mapping each time the user changes the locale in the application.
Thus, underneath we still want it linked to the context; but possibly the Global Context of the application behind the scenes without having to directly reference it.
We have two possible parameter slots in the MaterialApp widget to get an application Global Context. One is the NavigatorKey and one is the ScaffoldMessengerKey but since ScaffoldMessengerKey is the only one in MaterialApp.router constructor we can use that one for both the application and the MaterialApp wrapper used in testing.
Application Solution
We start out with some globals:
// Copyright 2024 Fredrick Allan Hrott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:flutter/material.dart'; | |
// So this is how we will get the context to use in any models as | |
// the scaffoldMessengerKey will be supplied to the MaterialApp | |
// and then the appRouterContext will be the context referred to | |
// in the AppLocalisations.of().localizationKey call in the model | |
// as this then can be repeated when we use the MaterialApp wrapper | |
// to unit test the model in unit tests. | |
final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); | |
BuildContext? appRouterContext = scaffoldMessengerKey.currentContext; |
Then the MaterialApp construct with the scaffoldMessengerKey parameter provided:
import 'package:better_localization_steps/src/app_globals.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | |
import 'package:flutter_localizations/flutter_localizations.dart'; | |
import 'sample_feature/sample_item_details_view.dart'; | |
import 'sample_feature/sample_item_list_view.dart'; | |
import 'settings/settings_controller.dart'; | |
import 'settings/settings_view.dart'; | |
/// The Widget that configures your application. | |
class MyApp extends StatelessWidget { | |
const MyApp({ | |
super.key, | |
required this.settingsController, | |
}); | |
final SettingsController settingsController; | |
@override | |
Widget build(BuildContext context) { | |
// Glue the SettingsController to the MaterialApp. | |
// | |
// The ListenableBuilder Widget listens to the SettingsController for changes. | |
// Whenever the user updates their settings, the MaterialApp is rebuilt. | |
return ListenableBuilder( | |
listenable: settingsController, | |
builder: (BuildContext context, Widget? child) { | |
return MaterialApp( | |
// Providing a restorationScopeId allows the Navigator built by the | |
// MaterialApp to restore the navigation stack when a user leaves and | |
// returns to the app after it has been killed while running in the | |
// background. | |
restorationScopeId: 'app', | |
// so we can get context outside of widget for things like models using | |
// localizations | |
scaffoldMessengerKey: scaffoldMessengerKey, | |
debugShowCheckedModeBanner: false, | |
// Provide the generated AppLocalizations to the MaterialApp. This | |
// allows descendant Widgets to display the correct translations | |
// depending on the user's locale. | |
localizationsDelegates: const [ | |
AppLocalizations.delegate, | |
GlobalMaterialLocalizations.delegate, | |
GlobalWidgetsLocalizations.delegate, | |
GlobalCupertinoLocalizations.delegate, | |
], | |
supportedLocales: const [ | |
Locale('en', ''), // English, no country code | |
], | |
// Use AppLocalizations to configure the correct application title | |
// depending on the user's locale. | |
// | |
// The appTitle is defined in .arb files found in the localization | |
// directory. | |
onGenerateTitle: (BuildContext context) => | |
AppLocalizations.of(context)!.appTitle, | |
// Define a light and dark color theme. Then, read the user's | |
// preferred ThemeMode (light, dark, or system default) from the | |
// SettingsController to display the correct theme. | |
theme: ThemeData(), | |
darkTheme: ThemeData.dark(), | |
themeMode: settingsController.themeMode, | |
// Define a function to handle named routes in order to support | |
// Flutter web url navigation and deep linking. | |
onGenerateRoute: (RouteSettings routeSettings) { | |
return MaterialPageRoute<void>( | |
settings: routeSettings, | |
builder: (BuildContext context) { | |
switch (routeSettings.name) { | |
case SettingsView.routeName: | |
return SettingsView(controller: settingsController); | |
case SampleItemDetailsView.routeName: | |
return const SampleItemDetailsView(); | |
case SampleItemListView.routeName: | |
default: | |
return const SampleItemListView(); | |
} | |
}, | |
); | |
}, | |
); | |
}, | |
); | |
} | |
} |
Keep in mind that the scaffoldMessengerKey can be used both the MaterialApp class and the MaterialApp.router construct. While the appRouterContext gets used in the models via:
AppLocalizations.of(appRouterContext).localizationKey; |
Now, for testing nothing is changed its just varied in how its declared
Testing Solution
In testing we generally have:
// Copyright 2024 Fredrick Allan Hrott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:flutter/material.dart'; | |
import 'package:flutter_localizations/flutter_localizations.dart'; | |
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | |
import 'package:flutter_test/flutter_test.dart'; | |
// General idea is the localizations in the model are referred to as | |
// AppLocalizations.of(appRouterContext).localizationKey | |
// so then we set the same scaffoldMessengerKey var | |
// and appRouterContext as in the app via MaterialApp wrapper | |
// pump the MaterialApp wrapper and child Container and | |
// then we can run tests on models that are using localizations. | |
// | |
// Because I use the scaffoldMessengerKey instead, the MaterialApp | |
// wrapper for the tests can be different than the MaterialApp.router | |
// in the app without any headaches. | |
late final scaffoldMessengerKey; // = GlobalKey<ScaffoldMessengerState>(); | |
void main() { | |
group('localizations tests', () { | |
setUpAll(() { | |
// sets up scaffoldMessengerKey for the MaterialApp wrapper we use | |
scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); | |
}); | |
testWidgets('so this would be a model test with localizations', (WidgetTester widgetTester) async { | |
await widgetTester.pumpWidget(MaterialApp( | |
scaffoldMessengerKey: scaffoldMessengerKey, | |
localizationsDelegates: const [ | |
AppLocalizations.delegate, | |
GlobalMaterialLocalizations.delegate, | |
GlobalWidgetsLocalizations.delegate, | |
GlobalCupertinoLocalizations.delegate, | |
], | |
supportedLocales: AppLocalizations.supportedLocales, | |
locale: const Locale('en', 'EN'), | |
home: Container(), | |
)); | |
// still need this according to bug issue 22193, large localizations file failure | |
await widgetTester.pump(); | |
// we do not have to search for Container element for context as we can do | |
BuildContext testContext = scaffoldMessengerKey.currentContext; | |
// and we do our expect testing here | |
}); | |
}); | |
} |
Note, to deal with large localization files we have to make it async along with doing two pumps, one with the MaterialApp wrapper and one after as that then sets up the scaffoldMessengerKey with the right Global Context that then can be used to test localizations. Also, note we set locale in the MaterialApp wrapper to the locale we want to test.
Conclusion
So, that is how to get the context to use outside of the widget to use localization in models and to then use it in testing to test the same model.