[flutter_tool] Reland: support --fast-start for Android applications (as an opt-in) (#46140)
diff --git a/dev/bots/test.dart b/dev/bots/test.dart index d0e8014..a98d0ef 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart
@@ -279,6 +279,13 @@ await selectSubshard(subshards); } +// Example apps that should not be built by _runBuildTests` +const List<String> _excludedExampleApplications = <String>[ + // This application contains no platform code and cannot be built, except for + // as a part of a '--fast-start' Android application. + 'splash', +]; + /// Verifies that AOT, APK, and IPA (if on macOS) builds the examples apps /// without crashing. It does not actually launch the apps. That happens later /// in the devicelab. This is just a smoke-test. In particular, this will verify @@ -290,6 +297,9 @@ if (fileEntity is! Directory) { continue; } + if (_excludedExampleApplications.any(fileEntity.path.endsWith)) { + continue; + } final String examplePath = fileEntity.path; await _flutterBuildAot(examplePath); await _flutterBuildApk(examplePath); @@ -763,6 +773,7 @@ if (Platform.isMacOS) () => _runDevicelabTest('flutter_create_offline_test_mac'), if (Platform.isLinux) () => _runDevicelabTest('flutter_create_offline_test_linux'), if (Platform.isWindows) () => _runDevicelabTest('flutter_create_offline_test_windows'), + () => _runDevicelabTest('gradle_fast_start_test', environment: gradleEnvironment), // TODO(ianh): Fails on macOS looking for "dexdump", https://github.com/flutter/flutter/issues/42494 if (!Platform.isMacOS) () => _runDevicelabTest('gradle_jetifier_test', environment: gradleEnvironment), () => _runDevicelabTest('gradle_non_android_plugin_test', environment: gradleEnvironment),
diff --git a/dev/devicelab/bin/tasks/gradle_fast_start_test.dart b/dev/devicelab/bin/tasks/gradle_fast_start_test.dart new file mode 100644 index 0000000..73fdf12 --- /dev/null +++ b/dev/devicelab/bin/tasks/gradle_fast_start_test.dart
@@ -0,0 +1,42 @@ +// 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. + +import 'dart:async'; + +import 'package:flutter_devicelab/framework/apk_utils.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; + +Future<void> main() async { + await task(() async { + try { + await runPluginProjectTest((FlutterPluginProject pluginProject) async { + section('APK content for task assembleDebug with --fast-start'); + await pluginProject.runGradleTask('assembleDebug', + options: <String>['-Pfast-start=true']); + + final Iterable<String> apkFiles = await getFilesInApk(pluginProject.debugApkPath); + + checkCollectionContains<String>(<String>[ + ...debugAssets, + ...baseApkFiles, + 'lib/x86/libflutter.so', + 'lib/x86_64/libflutter.so', + 'lib/armeabi-v7a/libflutter.so', + 'lib/arm64-v8a/libflutter.so', + ], apkFiles); + + checkCollectionDoesNotContain<String>(<String>[ + ...flutterAssets, + ], apkFiles); + }); + + return TaskResult.success(null); + } on TaskResult catch (taskResult) { + return taskResult; + } catch (e) { + return TaskResult.failure(e.toString()); + } + }); +}
diff --git a/examples/splash/lib/main.dart b/examples/splash/lib/main.dart new file mode 100644 index 0000000..3f55136 --- /dev/null +++ b/examples/splash/lib/main.dart
@@ -0,0 +1,16 @@ +// 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. + +import 'package:flutter/material.dart'; + +void main() { + runApp( + const DecoratedBox( + decoration: BoxDecoration(color: Colors.white), + child: Center( + child: FlutterLogo(size: 48), + ), + ), + ); +}
diff --git a/examples/splash/pubspec.yaml b/examples/splash/pubspec.yaml new file mode 100644 index 0000000..40a17ca --- /dev/null +++ b/examples/splash/pubspec.yaml
@@ -0,0 +1,41 @@ +name: splash + +environment: + # The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite. + sdk: ">=2.0.0-dev.68.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.1.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +dev_dependencies: + flutter_test: + sdk: flutter + + archive: 2.0.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + args: 1.5.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + async: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + boolean_selector: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + charcode: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + crypto: 2.1.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + image: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + matcher: 0.12.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path: 1.6.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + pedantic: 1.8.0+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + petitparser: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + quiver: 2.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_span: 1.5.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.9.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + string_scanner: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + term_glyph: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.2.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + xml: 3.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +# PUBSPEC CHECKSUM: f789
diff --git a/examples/splash/test/splash_test.dart b/examples/splash/test/splash_test.dart new file mode 100644 index 0000000..e1d597f --- /dev/null +++ b/examples/splash/test/splash_test.dart
@@ -0,0 +1,16 @@ +// 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. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:splash/main.dart' as entrypoint; + +void main() { + testWidgets('Displays flutter logo', (WidgetTester tester) async { + entrypoint.main(); + + expect(find.byType(FlutterLogo), findsOneWidget); + }); +}
diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle index fde4000..e852f2f 100644 --- a/packages/flutter_tools/gradle/flutter.gradle +++ b/packages/flutter_tools/gradle/flutter.gradle
@@ -472,6 +472,15 @@ return false } + /// Whether to build the debug app in "fast-start" mode. + private Boolean isFastStart() { + if (project.hasProperty("fast-start")) { + return project.property("fast-start").toBoolean() + } + return false + } + + private static Boolean shouldShrinkResources(Project project) { if (project.hasProperty("shrink")) { return project.property("shrink").toBoolean() @@ -610,6 +619,7 @@ localEngineSrcPath this.localEngineSrcPath targetPath target verbose isVerbose() + fastStart isFastStart() fileSystemRoots fileSystemRootsValue fileSystemScheme fileSystemSchemeValue trackWidgetCreation trackWidgetCreationValue @@ -729,6 +739,8 @@ String localEngine String localEngineSrcPath @Input + Boolean fastStart + @Input String targetPath @Optional Boolean verbose @@ -769,9 +781,13 @@ // cache. String[] ruleNames; if (buildMode == "debug") { - ruleNames = ["debug_android_application"] + if (fastStart) { + ruleNames = ["faststart_android_application"] + } else { + ruleNames = ["debug_android_application"] + } } else { - ruleNames = targetPlatformValues.collect { "android_aot_bundle_${buildMode}_$it" } + ruleNames = targetPlatformValues.collect { "android_aot_bundle_${buildMode}_$it" } } project.exec { executable flutterExecutable.absolutePath @@ -788,7 +804,11 @@ args "assemble" args "--depfile", "${intermediateDir}/flutter_build.d" args "--output", "${intermediateDir}" - args "-dTargetFile=${targetPath}" + if (!fastStart || buildMode != "debug") { + args "-dTargetFile=${targetPath}" + } else { + args "-dTargetFile=${Paths.get(flutterRoot.absolutePath, "examples", "splash", "lib", "main.dart")}" + } args "-dTargetPlatform=android" args "-dBuildMode=${buildMode}" if (extraFrontEndOptions != null) {
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index 0678841..5a523f6 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -527,6 +527,7 @@ androidBuildInfo: AndroidBuildInfo( debuggingOptions.buildInfo, targetArchs: <AndroidArch>[androidArch], + fastStart: debuggingOptions.fastStart ), ); // Package has been built, so we can get the updated application ID and @@ -644,6 +645,9 @@ bool get supportsHotRestart => true; @override + bool get supportsFastStart => true; + + @override Future<bool> stopApp(AndroidApk app) { final List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]); return processUtils.stream(command).then<bool>(
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 7f904c7..4440d98 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -331,6 +331,9 @@ // Don't use settings.gradle from the current project since it includes the plugins as subprojects. command.add('--settings-file=settings_aar.gradle'); } + if (androidBuildInfo.fastStart) { + command.add('-Pfast-start=true'); + } command.add(assembleTask); GradleHandledError detectedGradleError;
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 775461a..2f51d60 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -103,6 +103,7 @@ ], this.splitPerAbi = false, this.shrink = false, + this.fastStart = false, }); // The build info containing the mode and flavor. @@ -120,6 +121,9 @@ /// The target platforms for the build. final Iterable<AndroidArch> targetArchs; + + /// Whether to bootstrap an empty application. + final bool fastStart; } /// A summary of the compilation strategy used for Dart.
diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart index 36f1898..b13f632 100644 --- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
@@ -423,7 +423,7 @@ @override Future<OperationResult> restart({ bool fullRestart = false, - bool pauseAfterRestart = false, + bool pause = false, String reason, bool benchmarkMode = false, }) async { @@ -692,7 +692,7 @@ @override Future<OperationResult> restart({ bool fullRestart = false, - bool pauseAfterRestart = false, + bool pause = false, String reason, bool benchmarkMode = false, }) async {
diff --git a/packages/flutter_tools/lib/src/build_system/targets/android.dart b/packages/flutter_tools/lib/src/build_system/targets/android.dart index 5a030fb..1798d95 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/android.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/android.dart
@@ -31,10 +31,13 @@ List<Source> get outputs => const <Source>[]; @override - List<String> get depfiles => const <String>[ - 'flutter_assets.d', + List<String> get depfiles => <String>[ + if (_copyAssets) + 'flutter_assets.d', ]; + bool get _copyAssets => true; + @override Future<void> build(Environment environment) async { if (environment.defines[kBuildMode] == null) { @@ -56,8 +59,10 @@ fs.file(isolateSnapshotData) .copySync(outputDirectory.childFile('isolate_snapshot_data').path); } - final Depfile assetDepfile = await copyAssets(environment, outputDirectory); - assetDepfile.writeToFile(environment.buildDir.childFile('flutter_assets.d')); + if (_copyAssets) { + final Depfile assetDepfile = await copyAssets(environment, outputDirectory); + assetDepfile.writeToFile(environment.buildDir.childFile('flutter_assets.d')); + } } @override @@ -90,6 +95,17 @@ ]; } +/// A minimal android application that does not include assets. +class FastStartAndroidApplication extends DebugAndroidApplication { + const FastStartAndroidApplication(); + + @override + String get name => 'faststart_android_application'; + + @override + bool get _copyAssets => false; +} + /// An implementation of [AndroidAssetBundle] that only includes assets. class AotAndroidAssetBundle extends AndroidAssetBundle { const AotAndroidAssetBundle();
diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart index 74856e3..5265b5c 100644 --- a/packages/flutter_tools/lib/src/commands/assemble.dart +++ b/packages/flutter_tools/lib/src/commands/assemble.dart
@@ -37,6 +37,7 @@ DebugBundleLinuxAssets(), WebReleaseBundle(), DebugAndroidApplication(), + FastStartAndroidApplication(), ProfileAndroidApplication(), ReleaseAndroidApplication(), // These are one-off rules for bundle and aot compat
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 5c1b9db..a53b373 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -578,7 +578,7 @@ } _inProgressHotReload = app._runInZone<OperationResult>(this, () { - return app.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart, reason: restartReason); + return app.restart(fullRestart: fullRestart, pause: pauseAfterRestart, reason: restartReason); }); return _inProgressHotReload.whenComplete(() { _inProgressHotReload = null; @@ -945,8 +945,8 @@ _AppRunLogger _logger; - Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) { - return runner.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart, reason: reason); + Future<OperationResult> restart({ bool fullRestart = false, bool pause = false, String reason }) { + return runner.restart(fullRestart: fullRestart, pause: pause, reason: reason); } Future<OperationResult> reloadMethod({ String classId, String libraryId }) {
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index fb13cb7..222f207 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -187,6 +187,14 @@ hide: true, help: 'Whether to automatically invoke webOnlyInitializePlatform.', ) + ..addFlag('fast-start', + negatable: true, + defaultsTo: false, + hide: true, + help: 'Whether to quickly bootstrap applications with a minimal app. ' + 'Currently this is only supported on Android devices. This option ' + 'cannot be paired with --use-application-binary.' + ) ..addOption(FlutterOptions.kExtraFrontEndOptions, hide: true) ..addOption(FlutterOptions.kExtraGenSnapshotOptions, hide: true) ..addMultiOption(FlutterOptions.kEnableExperiment, @@ -284,6 +292,11 @@ if (!runningWithPrebuiltApplication) { await super.validateCommand(); } + + if (boolArg('fast-start') && runningWithPrebuiltApplication) { + throwToolExit('--fast-start is not supported with --use-application-binary'); + } + devices = await findAllTargetDevices(); if (devices == null) { throwToolExit(null); @@ -322,6 +335,9 @@ hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '', port: featureFlags.isWebEnabled ? stringArg('web-port') : '', vmserviceOutFile: stringArg('vmservice-out-file'), + // Allow forcing fast-start to off to prevent doing more work on devices that + // don't support it. + fastStart: boolArg('fast-start') && devices.every((Device device) => device.supportsFastStart), ); } } @@ -384,6 +400,12 @@ } for (Device device in devices) { + if (!device.supportsFastStart && boolArg('fast-start')) { + printStatus( + 'Using --fast-start option with device ${device.name}, but this device ' + 'does not support it. Overriding the setting to false.' + ); + } if (await device.isLocalEmulator) { if (await device.supportsHardwareRendering) { final bool enableSoftwareRendering = boolArg('enable-software-rendering') == true;
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 2b081ba..b335d3e 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart
@@ -420,6 +420,9 @@ /// application. bool get supportsScreenshot => false; + /// Whether the device supports the '--fast-start' development mode. + bool get supportsFastStart => false; + /// Stop an app package on the current device. Future<bool> stopApp(covariant ApplicationPackage app); @@ -534,6 +537,7 @@ this.hostname, this.port, this.vmserviceOutFile, + this.fastStart = false, }) : debuggingEnabled = true; DebuggingOptions.disabled(this.buildInfo, { this.initializePlatform = true, this.port, this.hostname, this.cacheSkSL = false, }) @@ -550,7 +554,8 @@ verboseSystemLogs = false, hostVmServicePort = null, deviceVmServicePort = null, - vmserviceOutFile = null; + vmserviceOutFile = null, + fastStart = false; final bool debuggingEnabled; @@ -574,6 +579,7 @@ final String hostname; /// A file where the vmservice URL should be written after the application is started. final String vmserviceOutFile; + final bool fastStart; bool get hasObservatoryPort => hostVmServicePort != null; }
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 0402b83..e666af6 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -714,7 +714,7 @@ bool get supportsRestart => false; - Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) { + Future<OperationResult> restart({ bool fullRestart = false, bool pause = false, String reason }) { final String mode = isRunningProfile ? 'profile' : isRunningRelease ? 'release' : 'this'; throw '${fullRestart ? 'Restart' : 'Reload'} is not supported in $mode mode';
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 9629942..a3fbe18 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -103,7 +103,7 @@ bool pause = false, }) async { // TODO(cbernaschina): check that isolateId is the id of the UI isolate. - final OperationResult result = await restart(pauseAfterRestart: pause); + final OperationResult result = await restart(pause: pause); if (!result.isOk) { throw rpc.RpcException( rpc_error_code.INTERNAL_ERROR, @@ -114,7 +114,7 @@ Future<void> _restartService({ bool pause = false }) async { final OperationResult result = - await restart(fullRestart: true, pauseAfterRestart: pause); + await restart(fullRestart: true, pause: pause); if (!result.isOk) { throw rpc.RpcException( rpc_error_code.INTERNAL_ERROR, @@ -263,6 +263,18 @@ } } + // In fast-start mode, apps are initialized from a placeholder splashscreen + // app. We must do a restart here to load the program and assets for the + // real app. + if (debuggingOptions.fastStart) { + await restart( + fullRestart: true, + benchmarkMode: !debuggingOptions.startPaused, + reason: 'restart', + silent: true, + ); + } + appStartedCompleter?.complete(); if (benchmarkMode) { @@ -408,12 +420,10 @@ Uri packagesUri, Uri assetsDirectoryUri, ) { - final List<Future<void>> futures = <Future<void>>[ - for (FlutterView view in device.views) view.runFromSource(entryUri, packagesUri, assetsDirectoryUri), - ]; - final Completer<void> completer = Completer<void>(); - Future.wait(futures).whenComplete(() { completer.complete(null); }); - return completer.future; + return Future.wait(<Future<void>>[ + for (FlutterView view in device.views) + view.runFromSource(entryUri, packagesUri, assetsDirectoryUri), + ]); } Future<void> _launchFromDevFS(String mainScript) async { @@ -510,9 +520,11 @@ for (FlutterDevice device in flutterDevices) { for (FlutterView view in device.views) { isolateNotifications.add( - view.owner.vm.vmService.onIsolateEvent.then((Stream<ServiceEvent> serviceEvents) async { + view.owner.vm.vmService.onIsolateEvent + .then((Stream<ServiceEvent> serviceEvents) async { await for (ServiceEvent serviceEvent in serviceEvents) { - if (serviceEvent.owner.name.contains('_spawn') && serviceEvent.kind == ServiceEvent.kIsolateExit) { + if (serviceEvent.owner.name.contains('_spawn') + && serviceEvent.kind == ServiceEvent.kIsolateExit) { return; } } @@ -573,9 +585,10 @@ @override Future<OperationResult> restart({ bool fullRestart = false, - bool pauseAfterRestart = false, String reason, bool benchmarkMode = false, + bool silent = false, + bool pause = false, }) async { String targetPlatform; String sdkName; @@ -602,8 +615,11 @@ emulator: emulator, reason: reason, benchmarkMode: benchmarkMode, + silent: silent, ); - printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.'); + if (!silent) { + printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.'); + } return result; } final OperationResult result = await _hotReloadHelper( @@ -611,11 +627,13 @@ sdkName: sdkName, emulator: emulator, reason: reason, - pauseAfterRestart: pauseAfterRestart, + pause: pause, ); if (result.isOk) { final String elapsed = getElapsedAsMilliseconds(timer.elapsed); - printStatus('${result.message} in $elapsed.'); + if (!silent) { + printStatus('${result.message} in $elapsed.'); + } } return result; } @@ -626,15 +644,19 @@ bool emulator, String reason, bool benchmarkMode, + bool silent, }) async { if (!canHotRestart) { return OperationResult(1, 'hotRestart not supported'); } - final Status status = logger.startProgress( - 'Performing hot restart...', - timeout: timeoutConfiguration.fastOperation, - progressId: 'hot.restart', - ); + Status status; + if (!silent) { + status = logger.startProgress( + 'Performing hot restart...', + timeout: timeoutConfiguration.fastOperation, + progressId: 'hot.restart', + ); + } OperationResult result; String restartEvent = 'restart'; try { @@ -662,7 +684,7 @@ emulator: emulator, fullRestart: true, reason: reason).send(); - status.cancel(); + status?.cancel(); } return result; } @@ -672,7 +694,7 @@ String sdkName, bool emulator, String reason, - bool pauseAfterRestart = false, + bool pause, }) async { final bool reloadOnTopOfSnapshot = _runningFromSnapshot; final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing'; @@ -687,8 +709,8 @@ targetPlatform: targetPlatform, sdkName: sdkName, emulator: emulator, - pause: pauseAfterRestart, reason: reason, + pause: pause, onSlow: (String message) { status?.cancel(); status = logger.startProgress(
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index 87a0a15..fad57ed 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart
@@ -11,12 +11,15 @@ import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/context.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.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/device.dart'; import 'package:flutter_tools/src/features.dart'; +import 'package:flutter_tools/src/globals.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/resident_runner.dart'; @@ -56,6 +59,72 @@ } }); + testUsingContext('does not support "--use-application-binary" and "--fast-start"', () async { + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + fs.file('pubspec.yaml').createSync(); + fs.file('.packages').createSync(); + + final RunCommand command = RunCommand(); + applyMocksToCommand(command); + try { + await createTestCommandRunner(command).run(<String>[ + 'run', + '--use-application-binary=app/bar/faz', + '--fast-start', + '--no-pub', + '--show-test-device', + ]); + fail('Expect exception'); + } catch (e) { + expect(e.toString(), contains('--fast-start is not supported with --use-application-binary')); + } + }, overrides: <Type, Generator>{ + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Forces fast start off for devices that do not support it', () async { + final MockDevice mockDevice = MockDevice(TargetPlatform.android_arm); + when(mockDevice.name).thenReturn('mockdevice'); + when(mockDevice.supportsFastStart).thenReturn(false); + when(mockDevice.supportsHotReload).thenReturn(true); + when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async => false); + when(deviceManager.hasSpecifiedAllDevices).thenReturn(false); + when(deviceManager.findTargetDevices(any)).thenAnswer((Invocation invocation) { + return Future<List<Device>>.value(<Device>[mockDevice]); + }); + when(deviceManager.getDevices()).thenAnswer((Invocation invocation) { + return Stream<Device>.value(mockDevice); + }); + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + fs.file('pubspec.yaml').createSync(); + fs.file('.packages').createSync(); + + final RunCommand command = RunCommand(); + applyMocksToCommand(command); + try { + await createTestCommandRunner(command).run(<String>[ + 'run', + '--fast-start', + '--no-pub', + ]); + fail('Expect exception'); + } catch (e) { + expect(e, isInstanceOf<ToolExit>()); + } + + final BufferLogger bufferLogger = logger as BufferLogger; + expect(bufferLogger.statusText, contains( + 'Using --fast-start option with device mockdevice, but this device ' + 'does not support it. Overriding the setting to false.' + )); + }, overrides: <Type, Generator>{ + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + DeviceManager: () => MockDeviceManager(), + }); + + group('run app', () { MemoryFileSystem fs; MockArtifacts mockArtifacts; @@ -166,6 +235,7 @@ final MockDevice mockDevice = MockDevice(TargetPlatform.ios); when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) => Future<bool>.value(false)); when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(MockDeviceLogReader()); + when(mockDevice.supportsFastStart).thenReturn(true); // App fails to start because we're only interested in usage when(mockDevice.startApp( any, @@ -498,6 +568,9 @@ bool get supportsHotReload => false; @override + bool get supportsFastStart => false; + + @override Future<String> get sdkNameAndVersion => Future<String>.value(''); @override
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index 39b55c6..0dd418d 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -154,6 +154,41 @@ expect(onAppStart.isCompleted, true); })); + test('ResidentRunner can attach to device successfully with --fast-start', () => testbed.run(() async { + when(mockDevice.supportsHotRestart).thenReturn(true); + when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { + return 'Example'; + }); + when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async { + return TargetPlatform.android_arm; + }); + when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async { + return false; + }); + residentRunner = HotRunner( + <FlutterDevice>[ + mockFlutterDevice, + ], + stayResident: false, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, fastStart: true, startPaused: true), + ); + final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync(); + final Completer<void> onAppStart = Completer<void>.sync(); + final Future<int> result = residentRunner.attach( + appStartedCompleter: onAppStart, + connectionInfoCompleter: onConnectionInfo, + ); + final Future<DebugConnectionInfo> connectionInfo = onConnectionInfo.future; + + expect(await result, 0); + + verify(mockFlutterDevice.initLogReader()).called(1); + + expect(onConnectionInfo.isCompleted, true); + expect((await connectionInfo).baseUri, 'foo://bar'); + expect(onAppStart.isCompleted, true); + })); + test('ResidentRunner can handle an RPC exception from hot reload', () => testbed.run(() async { when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { return 'Example';
diff --git a/packages/flutter_tools/test/integration.shard/test_driver.dart b/packages/flutter_tools/test/integration.shard/test_driver.dart index 4279b7a4..ff881e1 100644 --- a/packages/flutter_tools/test/integration.shard/test_driver.dart +++ b/packages/flutter_tools/test/integration.shard/test_driver.dart
@@ -497,7 +497,7 @@ // fast. unawaited(_process.exitCode.then((_) { if (!prematureExitGuard.isCompleted) { - prematureExitGuard.completeError('Process existed prematurely: ${args.join(' ')}'); + prematureExitGuard.completeError('Process existed prematurely: ${args.join(' ')}: $_errorBuffer'); } }));