mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
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.
This commit is contained in:
parent
5c2623d977
commit
d7fb51a551
|
@ -1,19 +1,104 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
String message = 'Flutter Debug Loader';
|
||||
String explanation = 'Please stand by...';
|
||||
double progress = 0.0;
|
||||
double progressMax = 0.0;
|
||||
StateSetter setState = (VoidCallback fn) => fn();
|
||||
Timer connectionTimeout;
|
||||
|
||||
void main() {
|
||||
runApp(new MaterialApp(
|
||||
title: 'Flutter Initial Load',
|
||||
new LoaderBinding();
|
||||
runApp(
|
||||
new MaterialApp(
|
||||
title: 'Flutter Debug Loader',
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: new Scaffold(
|
||||
body: new Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
new Text('Loading application onto device...',
|
||||
style: new TextStyle(fontSize: 24.0)),
|
||||
new CircularProgressIndicator(value: null)
|
||||
]
|
||||
body: new StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setStateRef) {
|
||||
setState = setStateRef;
|
||||
return new Column(
|
||||
children: <Widget>[
|
||||
new Flexible(
|
||||
child: new Container() // TODO(ianh): replace this with our logo in a Center box
|
||||
),
|
||||
new Flexible(
|
||||
child: new Builder(
|
||||
builder: (BuildContext context) {
|
||||
List<Widget> children = <Widget>[];
|
||||
children.add(new Text(
|
||||
message,
|
||||
style: new TextStyle(fontSize: 24.0),
|
||||
textAlign: TextAlign.center
|
||||
));
|
||||
if (progressMax >= 0.0) {
|
||||
children.add(new SizedBox(height: 18.0));
|
||||
children.add(new Center(child: new CircularProgressIndicator(value: progressMax > 0 ? progress / progressMax : null)));
|
||||
}
|
||||
return new Block(children: children);
|
||||
}
|
||||
)
|
||||
),
|
||||
new Flexible(
|
||||
child: new Block(
|
||||
padding: new EdgeInsets.symmetric(horizontal: 16.0),
|
||||
children: <Widget>[ new Text(explanation, textAlign: TextAlign.center) ]
|
||||
)
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
connectionTimeout = new Timer(const Duration(seconds: 8), () {
|
||||
setState(() {
|
||||
explanation =
|
||||
'This is a hot-reload-enabled debug-mode Flutter application. '
|
||||
'To launch this application, please use the "flutter run" command. '
|
||||
'To be able to launch a Flutter application in debug mode from the '
|
||||
'device, please use "flutter run --no-hot". To install a release '
|
||||
'mode build of this application on your device, use "flutter install".';
|
||||
progressMax = -1.0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class LoaderBinding extends WidgetsFlutterBinding {
|
||||
@override
|
||||
void initServiceExtensions() {
|
||||
super.initServiceExtensions();
|
||||
registerStringServiceExtension(
|
||||
name: 'loaderShowMessage',
|
||||
getter: () => message,
|
||||
setter: (String value) {
|
||||
connectionTimeout?.cancel();
|
||||
connectionTimeout = null;
|
||||
setState(() {
|
||||
message = value;
|
||||
});
|
||||
}
|
||||
);
|
||||
registerNumericServiceExtension(
|
||||
name: 'loaderSetProgress',
|
||||
getter: () => progress,
|
||||
setter: (double value) {
|
||||
setState(() {
|
||||
progress = value;
|
||||
});
|
||||
}
|
||||
);
|
||||
registerNumericServiceExtension(
|
||||
name: 'loaderSetProgressMax',
|
||||
getter: () => progressMax,
|
||||
setter: (double value) {
|
||||
setState(() {
|
||||
progressMax = value;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -203,6 +203,34 @@ abstract class BindingBase {
|
|||
);
|
||||
}
|
||||
|
||||
/// Registers a service extension method with the given name (full name
|
||||
/// "ext.flutter.name"), which optionally takes a single argument with the
|
||||
/// name "value". If the argument is omitted, the value is to be read,
|
||||
/// otherwise it is to be set. Returns the current value.
|
||||
///
|
||||
/// Calls the `getter` callback to obtain the value when
|
||||
/// responding to the service extension method being called.
|
||||
///
|
||||
/// Calls the `setter` callback with the new value when the
|
||||
/// service extension method is called with a new value.
|
||||
void registerStringServiceExtension({
|
||||
@required String name,
|
||||
@required ValueGetter<String> getter,
|
||||
@required ValueSetter<String> setter
|
||||
}) {
|
||||
assert(name != null);
|
||||
assert(getter != null);
|
||||
assert(setter != null);
|
||||
registerServiceExtension(
|
||||
name: name,
|
||||
callback: (Map<String, String> parameters) async {
|
||||
if (parameters.containsKey('value'))
|
||||
setter(parameters['value']);
|
||||
return <String, dynamic>{ 'value': getter() };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Registers a service extension method with the given name (full
|
||||
/// name "ext.flutter.name"). The given callback is called when the
|
||||
/// extension method is called. The callback must return a [Future]
|
||||
|
|
|
@ -56,13 +56,10 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
|
|||
return true;
|
||||
});
|
||||
|
||||
assert(() {
|
||||
registerSignalServiceExtension(
|
||||
name: 'debugDumpRenderTree',
|
||||
callback: debugDumpRenderTree
|
||||
);
|
||||
return true;
|
||||
});
|
||||
registerSignalServiceExtension(
|
||||
name: 'debugDumpRenderTree',
|
||||
callback: debugDumpRenderTree
|
||||
);
|
||||
|
||||
assert(() {
|
||||
// this service extension only works in checked mode
|
||||
|
|
|
@ -13,6 +13,7 @@ abstract class Logger {
|
|||
|
||||
bool quiet = false;
|
||||
|
||||
bool get supportsColor => terminal.supportsColor;
|
||||
set supportsColor(bool value) {
|
||||
terminal.supportsColor = value;
|
||||
}
|
||||
|
@ -76,7 +77,7 @@ class StdoutLogger extends Logger {
|
|||
_status?.cancel();
|
||||
_status = null;
|
||||
|
||||
if (terminal.supportsColor) {
|
||||
if (supportsColor) {
|
||||
_status = new _AnsiStatus(message);
|
||||
return _status;
|
||||
} else {
|
||||
|
|
|
@ -63,7 +63,7 @@ class RunCommand extends RunCommandBase {
|
|||
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
|
||||
|
|
|
@ -13,6 +13,8 @@ import 'asset.dart';
|
|||
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 @@ class DevFS {
|
|||
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 @@ class DevFS {
|
|||
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 @@ class DevFS {
|
|||
// 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.
|
||||
|
|
|
@ -9,6 +9,8 @@ import 'dart:io';
|
|||
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 @@ class Observatory {
|
|||
}).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>{
|
||||
|
|
|
@ -219,15 +219,6 @@ class RunAndStayResident {
|
|||
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 @@ class RunAndStayResident {
|
|||
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 @@ class RunAndStayResident {
|
|||
|
||||
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 @@ class RunAndStayResident {
|
|||
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 @@ class RunAndStayResident {
|
|||
}
|
||||
|
||||
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 @@ class RunAndStayResident {
|
|||
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 @@ class RunAndStayResident {
|
|||
}
|
||||
|
||||
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() {
|
||||
|
|
|
@ -58,17 +58,17 @@ void main() {
|
|||
expect(devFSOperations.contains('deleteFile test bar/foo.txt'), isTrue);
|
||||
});
|
||||
testUsingContext('add file in an asset bundle', () async {
|
||||
await devFS.update(assetBundle);
|
||||
await devFS.update(bundle: assetBundle);
|
||||
expect(devFSOperations.contains('writeFile test build/flx/a.txt'), isTrue);
|
||||
});
|
||||
testUsingContext('add a file to the asset bundle', () async {
|
||||
assetBundle.entries.add(new AssetBundleEntry.fromString('b.txt', ''));
|
||||
await devFS.update(assetBundle);
|
||||
await devFS.update(bundle: assetBundle);
|
||||
expect(devFSOperations.contains('writeFile test build/flx/b.txt'), isTrue);
|
||||
});
|
||||
testUsingContext('delete a file from the asset bundle', () async {
|
||||
assetBundle.entries.clear();
|
||||
await devFS.update(assetBundle);
|
||||
await devFS.update(bundle: assetBundle);
|
||||
expect(devFSOperations.contains('deleteFile test build/flx/b.txt'), isTrue);
|
||||
});
|
||||
testUsingContext('delete dev file system', () async {
|
||||
|
|
Loading…
Reference in a new issue