Hot reload UI polish (#5193)
* General improvoments to the loader app:
* Show a message after 8 seconds if no connection comes in.
* Show a progress bar as files are being uploaded.
* Hide the spinner just before launching the application.
* General improvements to the "flutter run" UI:
* Add "?" key as a silent alias for "h".
* Make the help text bold so it doesn't get mixed with the logs.
* Make "R" do a cold restart when hot reload is enabled.
* Supporting features and bug fixes:
* Add support for string service extensions.
* Other bug fixes:
* Expose debugDumpRenderTree() outside debug mode.
* Logger.supportsColor was missing a getter.
* Mention in the usage docs that --hot requires --resident.
* Trivial style fixes.
diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart
index 42bb2d9..4e16873 100644
--- a/packages/flutter_tools/lib/src/base/logger.dart
+++ b/packages/flutter_tools/lib/src/base/logger.dart
@@ -13,6 +13,7 @@
bool quiet = false;
+ bool get supportsColor => terminal.supportsColor;
set supportsColor(bool value) {
terminal.supportsColor = value;
}
@@ -76,7 +77,7 @@
_status?.cancel();
_status = null;
- if (terminal.supportsColor) {
+ if (supportsColor) {
_status = new _AnsiStatus(message);
return _status;
} else {
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index f4b5175..cd14ae2 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -63,7 +63,7 @@
argParser.addFlag('hot',
negatable: false,
defaultsTo: false,
- help: 'Run with support for hot reloading.');
+ help: 'Run with support for hot reloading. Requires resident.');
// Hidden option to enable a benchmarking mode. This will run the given
// application, measure the startup time and the app restart time, write the
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
index c46fac9..2d4e4f0 100644
--- a/packages/flutter_tools/lib/src/devfs.dart
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -13,6 +13,8 @@
import 'globals.dart';
import 'observatory.dart';
+typedef void DevFSProgressReporter(int progress, int max);
+
// A file that has been added to a DevFS.
class DevFSEntry {
DevFSEntry(this.devicePath, this.file)
@@ -178,7 +180,7 @@
return await _operations.destroy(fsName);
}
- Future<dynamic> update([AssetBundle bundle = null]) async {
+ Future<dynamic> update({ DevFSProgressReporter progressReporter, AssetBundle bundle }) async {
_bytes = 0;
// Mark all entries as not seen.
_entries.forEach((String path, DevFSEntry entry) {
@@ -203,9 +205,7 @@
if (_syncDirectory(directory,
directoryName: 'packages/$packageName',
recursive: true)) {
- if (sb == null) {
- sb = new StringBuffer();
- }
+ sb ??= new StringBuffer();
sb.writeln('$packageName:packages/$packageName');
}
}
@@ -233,11 +233,20 @@
// Send the assets.
printTrace('DevFS: Waiting for sync of ${_pendingWrites.length} files '
'to finish');
- await Future.wait(_pendingWrites);
- _pendingWrites.clear();
- if (sb != null) {
- await _operations.writeSource(fsName, '.packages', sb.toString());
+
+ if (progressReporter != null) {
+ final int max = _pendingWrites.length;
+ int complete = 0;
+ _pendingWrites.forEach((Future<dynamic> f) => f.then((dynamic v) {
+ complete += 1;
+ progressReporter(complete, max);
+ }));
}
+ await Future.wait(_pendingWrites, eagerError: true);
+ _pendingWrites.clear();
+
+ if (sb != null)
+ await _operations.writeSource(fsName, '.packages', sb.toString());
printTrace('DevFS: Sync finished');
// NB: You must call flush after a printTrace if you want to be printed
// immediately.
diff --git a/packages/flutter_tools/lib/src/observatory.dart b/packages/flutter_tools/lib/src/observatory.dart
index c04c9b9..b7d3337 100644
--- a/packages/flutter_tools/lib/src/observatory.dart
+++ b/packages/flutter_tools/lib/src/observatory.dart
@@ -9,6 +9,8 @@
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:web_socket_channel/io.dart';
+import 'globals.dart';
+
// TODO(johnmccutchan): Rename this class to ServiceProtocol or VmService.
class Observatory {
Observatory._(this.peer, this.port) {
@@ -204,6 +206,38 @@
}).then((dynamic result) => new Response(result));
}
+ // Loader page extension methods.
+
+ Future<Response> flutterLoaderShowMessage(String isolateId, String message) {
+ return peer.sendRequest('ext.flutter.loaderShowMessage', <String, dynamic>{
+ 'isolateId': isolateId,
+ 'value': message
+ }).then(
+ (dynamic result) => new Response(result),
+ onError: (dynamic exception) { printTrace('ext.flutter.loaderShowMessage: $exception'); }
+ );
+ }
+
+ Future<Response> flutterLoaderSetProgress(String isolateId, double progress) {
+ return peer.sendRequest('ext.flutter.loaderSetProgress', <String, dynamic>{
+ 'isolateId': isolateId,
+ 'loaderSetProgress': progress
+ }).then(
+ (dynamic result) => new Response(result),
+ onError: (dynamic exception) { printTrace('ext.flutter.loaderSetProgress: $exception'); }
+ );
+ }
+
+ Future<Response> flutterLoaderSetProgressMax(String isolateId, double max) {
+ return peer.sendRequest('ext.flutter.loaderSetProgressMax', <String, dynamic>{
+ 'isolateId': isolateId,
+ 'loaderSetProgressMax': max
+ }).then(
+ (dynamic result) => new Response(result),
+ onError: (dynamic exception) { printTrace('ext.flutter.loaderSetProgressMax: $exception'); }
+ );
+ }
+
/// Causes the application to pick up any changed code.
Future<Response> flutterReassemble(String isolateId) {
return peer.sendRequest('ext.flutter.reassemble', <String, dynamic>{
diff --git a/packages/flutter_tools/lib/src/run.dart b/packages/flutter_tools/lib/src/run.dart
index 7eac9b7..1ee4eab 100644
--- a/packages/flutter_tools/lib/src/run.dart
+++ b/packages/flutter_tools/lib/src/run.dart
@@ -219,15 +219,6 @@
if (debuggingOptions.debuggingEnabled) {
observatory = await Observatory.connect(_result.observatoryPort);
printTrace('Connected to observatory port: ${_result.observatoryPort}.');
- if (hotMode && device.needsDevFS) {
- bool result = await _updateDevFS();
- if (!result) {
- printError('Could not perform initial file synchronization.');
- return 3;
- }
- printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...');
- await _launchFromDevFS(_package, _mainPath);
- }
observatory.populateIsolateInfo();
observatory.onExtensionEvent.listen((Event event) {
printTrace(event.toString());
@@ -236,6 +227,23 @@
printTrace(event.toString());
});
+ if (hotMode && device.needsDevFS) {
+ _loaderShowMessage('Connecting...', progress: 0);
+ bool result = await _updateDevFS(
+ progressReporter: (int progress, int max) {
+ _loaderShowMessage('Syncing files to device...', progress: progress, max: max);
+ }
+ );
+ if (!result) {
+ _loaderShowMessage('Failed.');
+ printError('Could not perform initial file synchronization.');
+ return 3;
+ }
+ printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...');
+ _loaderShowMessage('Launching...');
+ await _launchFromDevFS(_package, _mainPath);
+ }
+
if (benchmark)
await observatory.waitFirstIsolate;
@@ -264,19 +272,20 @@
terminal.singleCharMode = true;
terminal.onCharInput.listen((String code) {
- String lower = code.toLowerCase();
-
- if (lower == 'h' || code == AnsiTerminal.KEY_F1) {
+ printStatus(''); // the key the user tapped might be on this line
+ final String lower = code.toLowerCase();
+ if (lower == 'h' || lower == '?' || code == AnsiTerminal.KEY_F1) {
// F1, help
_printHelp();
} else if (lower == 'r' || code == AnsiTerminal.KEY_F5) {
- if (hotMode) {
+ // F5, restart
+ if (hotMode && code == 'r') {
+ // lower-case 'r'
_reloadSources();
} else {
- if (device.supportsRestart) {
- // F5, restart
+ // upper-case 'r', or hot restart disabled
+ if (device.supportsRestart)
restart();
- }
}
} else if (lower == 'q' || code == AnsiTerminal.KEY_F10) {
// F10, exit
@@ -335,9 +344,20 @@
observatory.flutterDebugDumpRenderTree(observatory.firstIsolateId);
}
+ void _loaderShowMessage(String message, { int progress, int max }) {
+ observatory.flutterLoaderShowMessage(observatory.firstIsolateId, message);
+ if (progress != null) {
+ observatory.flutterLoaderSetProgress(observatory.firstIsolateId, progress.toDouble());
+ observatory.flutterLoaderSetProgressMax(observatory.firstIsolateId, max?.toDouble() ?? 0.0);
+ } else {
+ observatory.flutterLoaderSetProgress(observatory.firstIsolateId, 0.0);
+ observatory.flutterLoaderSetProgressMax(observatory.firstIsolateId, -1.0);
+ }
+ }
+
DevFS _devFS;
String _devFSProjectRootPath;
- Future<bool> _updateDevFS() async {
+ Future<bool> _updateDevFS({ DevFSProgressReporter progressReporter }) async {
if (_devFS == null) {
Directory directory = Directory.current;
_devFSProjectRootPath = directory.path;
@@ -358,7 +378,7 @@
}
Status devFSStatus = logger.startProgress('Syncing files on device...');
- await _devFS.update();
+ await _devFS.update(progressReporter: progressReporter);
devFSStatus.stop(showElapsedTime: true);
printStatus('Synced ${getSizeAsMB(_devFS.bytes)} MB');
return true;
@@ -387,9 +407,8 @@
Future<bool> _reloadSources() async {
if (observatory.firstIsolateId == null)
throw 'Application isolate not found';
- if (_devFS != null) {
+ if (_devFS != null)
await _updateDevFS();
- }
Status reloadStatus = logger.startProgress('Performing hot reload');
try {
await observatory.reloadSources(observatory.firstIsolateId);
@@ -413,14 +432,21 @@
}
void _printHelp() {
- String restartText = '';
- if (hotMode) {
- restartText = ', "r" or F5 to perform a hot reload of the app,';
- } else if (device.supportsRestart) {
- restartText = ', "r" or F5 to restart the app,';
+ printStatus('Type "h" or F1 for this help message. Type "q", F10, or ctrl-c to quit.', emphasis: true);
+ String hot = '';
+ String cold = '';
+ if (hotMode)
+ hot = 'Type "r" or F5 to perform a hot reload of the app';
+ if (device.supportsRestart) {
+ if (hotMode) {
+ cold = ', and "R" to cold restart the app';
+ } else {
+ cold = 'Type "r" or F5 to restart the app';
+ }
}
- printStatus('Type "h" or F1 for help$restartText and "q", F10, or ctrl-c to quit.');
- printStatus('Type "w" to print the widget hierarchy of the app, and "t" for the render tree.');
+ if (hot != '' || cold != '')
+ printStatus('$hot$cold.', emphasis: true);
+ printStatus('Type "w" to print the widget hierarchy of the app, and "t" for the render tree.', emphasis: true);
}
Future<dynamic> _stopLogger() {