diff --git a/packages/flutter_tools/lib/src/commands/generate_localizations.dart b/packages/flutter_tools/lib/src/commands/generate_localizations.dart index 47775b5d0fd..2df58125df5 100644 --- a/packages/flutter_tools/lib/src/commands/generate_localizations.dart +++ b/packages/flutter_tools/lib/src/commands/generate_localizations.dart @@ -172,6 +172,17 @@ class GenerateLocalizationsCommand extends FlutterCommand { '\n' 'Resource attributes are still required for plural messages.' ); + argParser.addFlag( + 'nullable-getter', + help: 'Whether or not the localizations class getter is nullable.\n' + '\n' + 'By default, this value is set to true so that ' + 'Localizations.of(context) returns a nullable value ' + 'for backwards compatibility. If this value is set to true, then ' + 'a null check is performed on the returned value of ' + 'Localizations.of(context), removing the need for null checking in ' + 'user code.' + ); } final FileSystem _fileSystem; @@ -220,6 +231,7 @@ class GenerateLocalizationsCommand extends FlutterCommand { final bool useSyntheticPackage = boolArg('synthetic-package'); final String projectPathString = stringArg('project-dir'); final bool areResourceAttributesRequired = boolArg('required-resource-attributes'); + final bool usesNullableGetter = boolArg('nullable-getter'); final LocalizationsGenerator localizationsGenerator = LocalizationsGenerator(_fileSystem); @@ -242,6 +254,7 @@ class GenerateLocalizationsCommand extends FlutterCommand { projectPathString: projectPathString, areResourceAttributesRequired: areResourceAttributesRequired, untranslatedMessagesFile: untranslatedMessagesFile, + usesNullableGetter: usesNullableGetter, ) ..loadResources() ..writeOutputFiles(_logger); diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart index 3d26363599b..b6553497d49 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart @@ -66,6 +66,7 @@ void generateLocalizations({ useSyntheticPackage: options.useSyntheticPackage ?? true, areResourceAttributesRequired: options.areResourceAttributesRequired ?? false, untranslatedMessagesFile: options?.untranslatedMessagesFile?.toFilePath(), + usesNullableGetter: options?.usesNullableGetter ?? true, ) ..loadResources() ..writeOutputFiles(logger, isFromYaml: true); @@ -545,6 +546,10 @@ class LocalizationsGenerator { AppResourceBundleCollection _allBundles; LocaleInfo _templateArbLocale; bool _useSyntheticPackage = true; + // Used to decide if the generated code is nullable or not + // (whether AppLocalizations? or AppLocalizations is returned from + // `static {name}Localizations{?} of (BuildContext context))` + bool _usesNullableGetter = true; /// The directory that contains the project's arb files, as well as the /// header file, if specified. @@ -689,8 +694,10 @@ class LocalizationsGenerator { String projectPathString, bool areResourceAttributesRequired = false, String untranslatedMessagesFile, + bool usesNullableGetter = true, }) { _useSyntheticPackage = useSyntheticPackage; + _usesNullableGetter = usesNullableGetter; setProjectDir(projectPathString); setInputDirectory(inputPathString); setOutputDirectory(outputPathString ?? inputPathString); @@ -1162,7 +1169,9 @@ class LocalizationsGenerator { .replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.join(', ')) .replaceAll('@(messageClassImports)', sortedClassImports.join('\n')) .replaceAll('@(delegateClass)', delegateClass) - .replaceAll('@(requiresIntlImport)', _containsPluralMessage() ? "import 'package:intl/intl.dart' as intl;" : ''); + .replaceAll('@(requiresIntlImport)', _containsPluralMessage() ? "import 'package:intl/intl.dart' as intl;" : '') + .replaceAll('@(canBeNullable)', _usesNullableGetter ? '?' : '') + .replaceAll('@(needsNullCheck)', _usesNullableGetter ? '' : '!'); } bool _containsPluralMessage() => _allMessages.any((Message message) => message.isPlural); diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart index 01f2c7540fe..da7cc5098d7 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n_templates.dart @@ -77,8 +77,8 @@ abstract class @(class) { // ignore: unused_field final String localeName; - static @(class)? of(BuildContext context) { - return Localizations.of<@(class)>(context, @(class)); + static @(class)@(canBeNullable) of(BuildContext context) { + return Localizations.of<@(class)>(context, @(class))@(needsNullCheck); } static const LocalizationsDelegate<@(class)> delegate = _@(class)Delegate(); diff --git a/packages/flutter_tools/lib/src/localizations/localizations_utils.dart b/packages/flutter_tools/lib/src/localizations/localizations_utils.dart index 393e80eb3e8..dd221a0237f 100644 --- a/packages/flutter_tools/lib/src/localizations/localizations_utils.dart +++ b/packages/flutter_tools/lib/src/localizations/localizations_utils.dart @@ -304,6 +304,7 @@ class LocalizationOptions { this.deferredLoading, this.useSyntheticPackage = true, this.areResourceAttributesRequired = false, + this.usesNullableGetter = true, }) : assert(useSyntheticPackage != null); /// The `--arb-dir` argument. @@ -365,6 +366,11 @@ class LocalizationOptions { /// Whether to require all resource ids to contain a corresponding /// resource attribute. final bool areResourceAttributesRequired; + + /// The `nullable-getter` argument. + /// + /// Whether or not the localizations class getter is nullable. + final bool usesNullableGetter; } /// Parse the localizations configuration options from [file]. @@ -398,6 +404,7 @@ LocalizationOptions parseLocalizationsOptions({ deferredLoading: _tryReadBool(yamlNode, 'use-deferred-loading', logger), useSyntheticPackage: _tryReadBool(yamlNode, 'synthetic-package', logger) ?? true, areResourceAttributesRequired: _tryReadBool(yamlNode, 'required-resource-attributes', logger) ?? false, + usesNullableGetter: _tryReadBool(yamlNode, 'nullable-getter', logger) ?? true, ); } diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/localizations_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/localizations_test.dart index 207815969d8..810201f8639 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/localizations_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/localizations_test.dart @@ -44,6 +44,7 @@ void main() { untranslatedMessagesFile: Uri.file('untranslated'), useSyntheticPackage: false, areResourceAttributesRequired: true, + usesNullableGetter: false, ); final LocalizationsGenerator mockLocalizationsGenerator = MockLocalizationsGenerator(); @@ -70,6 +71,7 @@ void main() { projectPathString: '/', areResourceAttributesRequired: true, untranslatedMessagesFile: 'untranslated', + usesNullableGetter: false, ), ).called(1); verify(mockLocalizationsGenerator.loadResources()).called(1); @@ -151,6 +153,9 @@ header-file: header header: HEADER use-deferred-loading: true preferred-supported-locales: en_US +synthetic-package: false +required-resource-attributes: false +nullable-getter: false '''); final LocalizationOptions options = parseLocalizationsOptions( @@ -167,6 +172,9 @@ preferred-supported-locales: en_US expect(options.header, 'HEADER'); expect(options.deferredLoading, true); expect(options.preferredSupportedLocales, ['en_US']); + expect(options.useSyntheticPackage, false); + expect(options.areResourceAttributesRequired, false); + expect(options.usesNullableGetter, false); }); testWithoutContext('parseLocalizationsOptions handles preferredSupportedLocales as list', () async { diff --git a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart index 0745daf2204..d5b1a6b5ded 100644 --- a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart +++ b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart @@ -800,6 +800,82 @@ void main() { }, ); + testUsingContext( + 'generates nullable localizations class getter via static `of` method ' + 'by default', + () { + _standardFlutterDirectoryL10nSetup(fs); + + LocalizationsGenerator generator; + try { + generator = LocalizationsGenerator(fs); + generator + ..initialize( + inputPathString: defaultL10nPathString, + outputPathString: fs.path.join('lib', 'l10n', 'output'), + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + useSyntheticPackage: false, + ) + ..loadResources() + ..writeOutputFiles(BufferLogger.test()); + } on L10nException catch (e) { + fail('Generating output should not fail: \n${e.message}'); + } + + final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output'); + expect(outputDirectory.existsSync(), isTrue); + expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); + expect( + outputDirectory.childFile('output-localization-file.dart').readAsStringSync(), + contains('static AppLocalizations? of(BuildContext context)'), + ); + expect( + outputDirectory.childFile('output-localization-file.dart').readAsStringSync(), + contains('return Localizations.of(context, AppLocalizations);'), + ); + }, + ); + + testUsingContext( + 'can generate non-nullable localizations class getter via static `of` method ', + () { + _standardFlutterDirectoryL10nSetup(fs); + + LocalizationsGenerator generator; + try { + generator = LocalizationsGenerator(fs); + generator + ..initialize( + inputPathString: defaultL10nPathString, + outputPathString: fs.path.join('lib', 'l10n', 'output'), + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + useSyntheticPackage: false, + usesNullableGetter: false, + ) + ..loadResources() + ..writeOutputFiles(BufferLogger.test()); + } on L10nException catch (e) { + fail('Generating output should not fail: \n${e.message}'); + } + + final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output'); + expect(outputDirectory.existsSync(), isTrue); + expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); + expect( + outputDirectory.childFile('output-localization-file.dart').readAsStringSync(), + contains('static AppLocalizations of(BuildContext context)'), + ); + expect( + outputDirectory.childFile('output-localization-file.dart').readAsStringSync(), + contains('return Localizations.of(context, AppLocalizations)!;'), + ); + }, + ); + testUsingContext('creates list of inputs and outputs when file path is specified', () { _standardFlutterDirectoryL10nSetup(fs);