Add API to read flavor from framework at run time (#134179)

Resolves #128046.

Adds a services API that allows flutter app developers to write app code that determines `--flavor` the app was built with.

This is implemented by having the tool adding the value of `--flavor` to its list of dart environment declarations, which will be available to the app at run time. Specifically,`FLUTTER_APP_FLAVOR` is set. I chose this implementation for its simplicity. There is some precedent for this, but only for web ([example](cd2f3f5e78/packages/flutter_tools/lib/src/runner/flutter_command.dart (L1231))).
This commit is contained in:
Andrew Kolos 2023-09-28 10:38:54 -07:00 committed by GitHub
parent c35515a079
commit aa498cd51a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 109 additions and 3 deletions

View file

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flavors/main.dart' as app;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -16,6 +17,7 @@ void main() {
await tester.pumpAndSettle();
expect(find.text('paid'), findsOneWidget);
expect(appFlavor, 'paid');
});
});
}

View file

@ -19,6 +19,7 @@ export 'src/services/browser_context_menu.dart';
export 'src/services/clipboard.dart';
export 'src/services/debug.dart';
export 'src/services/deferred_component.dart';
export 'src/services/flavor.dart';
export 'src/services/font_loader.dart';
export 'src/services/haptic_feedback.dart';
export 'src/services/hardware_keyboard.dart';

View file

@ -0,0 +1,10 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// The flavor this app was built with.
///
/// This is equivalent to the value argued to the `--flavor` option at build time.
/// This will be `null` if the `--flavor` option was not provided.
const String? appFlavor = String.fromEnvironment('FLUTTER_APP_FLAVOR') != '' ?
String.fromEnvironment('FLUTTER_APP_FLAVOR') : null;

View file

@ -1247,10 +1247,20 @@ abstract class FlutterCommand extends Command<void> {
}
}
final String? flavor = argParser.options.containsKey('flavor') ? stringArg('flavor') : null;
if (flavor != null) {
if (globals.platform.environment['FLUTTER_APP_FLAVOR'] != null) {
throwToolExit('FLUTTER_APP_FLAVOR is used by the framework and cannot be set in the environment.');
}
if (dartDefines.any((String define) => define.startsWith('FLUTTER_APP_FLAVOR'))) {
throwToolExit('FLUTTER_APP_FLAVOR is used by the framework and cannot be '
'set using --${FlutterOptions.kDartDefinesOption} or --${FlutterOptions.kDartDefineFromFileOption}');
}
dartDefines.add('FLUTTER_APP_FLAVOR=$flavor');
}
return BuildInfo(buildMode,
argParser.options.containsKey('flavor')
? stringArg('flavor')
: null,
flavor,
trackWidgetCreation: trackWidgetCreation,
frontendServerStarterPath: argParser.options
.containsKey(FlutterOptions.kFrontendServerStarterPath)

View file

@ -11,11 +11,14 @@ import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/error_handling_io.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/signals.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/run.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
@ -700,6 +703,67 @@ void main() {
expect(testLogger.statusText, contains(UserMessages().flutterSpecifyDevice));
});
});
group('--flavor', () {
late _TestDeviceManager testDeviceManager;
late Logger logger;
late FileSystem fileSystem;
setUp(() {
logger = BufferLogger.test();
testDeviceManager = _TestDeviceManager(logger: logger);
fileSystem = MemoryFileSystem.test();
});
testUsingContext("tool exits when FLUTTER_APP_FLAVOR is already set in user's environment", () async {
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages').createSync();
final FakeDevice device = FakeDevice('name', 'id');
testDeviceManager.devices = <Device>[device];
final _TestRunCommandThatOnlyValidates command = _TestRunCommandThatOnlyValidates();
final CommandRunner<void> runner = createTestCommandRunner(command);
expect(runner.run(<String>['run', '--no-pub', '--no-hot', '--flavor=strawberry']),
throwsToolExit(message: 'FLUTTER_APP_FLAVOR is used by the framework and cannot be set in the environment.'));
}, overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Platform: () => FakePlatform(
environment: <String, String>{
'FLUTTER_APP_FLAVOR': 'I was already set'
}
),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('tool exits when FLUTTER_APP_FLAVOR is set in --dart-define or --dart-define-from-file', () async {
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages').createSync();
fileSystem.file('config.json')..createSync()..writeAsStringSync('{"FLUTTER_APP_FLAVOR": "strawberry"}');
final FakeDevice device = FakeDevice('name', 'id');
testDeviceManager.devices = <Device>[device];
final _TestRunCommandThatOnlyValidates command = _TestRunCommandThatOnlyValidates();
final CommandRunner<void> runner = createTestCommandRunner(command);
expect(runner.run(<String>['run', '--dart-define=FLUTTER_APP_FLAVOR=strawberry', '--no-pub', '--no-hot', '--flavor=strawberry']),
throwsToolExit(message: 'FLUTTER_APP_FLAVOR is used by the framework and cannot be set using --dart-define or --dart-define-from-file'));
expect(runner.run(<String>['run', '--dart-define-from-file=config.json', '--no-pub', '--no-hot', '--flavor=strawberry']),
throwsToolExit(message: 'FLUTTER_APP_FLAVOR is used by the framework and cannot be set using --dart-define or --dart-define-from-file'));
}, overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Platform: () => FakePlatform(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
});
});
}
@ -853,3 +917,22 @@ class FakePub extends Fake implements Pub {
PubOutputMode outputMode = PubOutputMode.all,
}) async { }
}
class _TestDeviceManager extends DeviceManager {
_TestDeviceManager({required super.logger});
List<Device> devices = <Device>[];
@override
List<DeviceDiscovery> get deviceDiscoverers {
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
devices.forEach(discoverer.addDevice);
return <DeviceDiscovery>[discoverer];
}
}
class _TestRunCommandThatOnlyValidates extends RunCommand {
@override
Future<FlutterCommandResult> runCommand() async {
return FlutterCommandResult.success();
}
}