Unverified Commit 58811028 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter] Allow hot reload replacements of Stateless/Stateful Widget (#48932)

parent 211ecc86
...@@ -11,6 +11,7 @@ import 'package:flutter/rendering.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter/rendering.dart';
import 'debug.dart'; import 'debug.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
import 'inherited_model.dart';
export 'dart:ui' show hashValues, hashList; export 'dart:ui' show hashValues, hashList;
...@@ -473,6 +474,24 @@ abstract class Widget extends DiagnosticableTree { ...@@ -473,6 +474,24 @@ abstract class Widget extends DiagnosticableTree {
return oldWidget.runtimeType == newWidget.runtimeType return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key; && oldWidget.key == newWidget.key;
} }
// Return a numeric encoding of the specific `Widget` concrete subtype.
// This is used in `Element.updateChild` to determine if a hot reload modified the
// superclass of a mounted element's configuration. The encoding of each `Widget`
// must match the corresponding `Element` encoding in `Element._debugConcreteSubtype`.
static int _debugConcreteSubtype(Widget widget) {
return widget is StatefulWidget ? 1 :
widget is StatelessWidget ? 2 :
widget is InheritedModel ? 3 :
widget is InheritedWidget ? 4 :
widget is ParentDataWidget ? 5 :
widget is ProxyWidget ? 6 :
widget is LeafRenderObjectWidget ? 7 :
widget is SingleChildRenderObjectWidget? 8 :
widget is MultiChildRenderObjectWidget ? 9 :
widget is RenderObjectWidget ? 10 :
0;
}
} }
/// A widget that does not require mutable state. /// A widget that does not require mutable state.
...@@ -2838,6 +2857,24 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -2838,6 +2857,24 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
return 0; return 0;
} }
// Return a numeric encoding of the specific `Element` concrete subtype.
// This is used in `Element.updateChild` to determine if a hot reload modified the
// superclass of a mounted element's configuration. The encoding of each `Element`
// must match the corresponding `Widget` encoding in `Widget._debugConcreteSubtype`.
static int _debugConcreteSubtype(Element element) {
return element is StatefulElement ? 1 :
element is StatelessElement ? 2 :
element is InheritedModelElement ? 3 :
element is InheritedElement ? 4 :
element is ParentDataElement ? 5 :
element is ProxyElement ? 6 :
element is LeafRenderObjectElement ? 7 :
element is SingleChildRenderObjectElement ? 8 :
element is MultiChildRenderObjectElement ? 9 :
element is RenderObjectElement ? 10 :
0;
}
/// The configuration for this element. /// The configuration for this element.
@override @override
Widget get widget => _widget; Widget get widget => _widget;
...@@ -3069,21 +3106,45 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3069,21 +3106,45 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
return null; return null;
} }
if (child != null) { if (child != null) {
if (child.widget == newWidget) { bool hasSameSuperclass = true;
if (child.slot != newSlot) // When the type of a widget is changed between Stateful and Stateless via
updateSlotForChild(child, newSlot); // hot reload, the element tree will end up in a partially invalid state.
return child; // That is, if the widget was a StatefulWidget and is now a StatelessWidget,
} // then the element tree currently contains a StatefulElement that is incorrectly
if (Widget.canUpdate(child.widget, newWidget)) { // referencing a StatelessWidget (and likewise with StatelessElement).
if (child.slot != newSlot) //
updateSlotForChild(child, newSlot); // To avoid crashing due to type errors, we need to gently guide the invalid
child.update(newWidget); // element out of the tree. To do so, we ensure that the `hasSameSuperclass` condition
assert(child.widget == newWidget); // returns false which prevents us from trying to update the existing element
assert(() { // incorrectly.
child.owner._debugElementWasRebuilt(child); //
return true; // For the case where the widget becomes Stateful, we also need to avoid
}()); // accessing `StatelessElement.widget` as the cast on the getter will
return child; // cause a type error to be thrown. Here we avoid that by short-circuiting
// the `Widget.canUpdate` check once `hasSameSuperclass` is false.
assert(() {
final int oldElementClass = Element._debugConcreteSubtype(child);
final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
hasSameSuperclass = oldElementClass == newWidgetClass;
return true;
}());
if (hasSameSuperclass) {
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
assert(child.widget == newWidget);
assert(() {
child.owner._debugElementWasRebuilt(child);
return true;
}());
return child;
}
} }
deactivateChild(child); deactivateChild(child);
assert(child._parent == null); assert(child._parent == null);
...@@ -3424,7 +3485,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3424,7 +3485,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
@mustCallSuper @mustCallSuper
void deactivate() { void deactivate() {
assert(_debugLifecycleState == _ElementLifecycle.active); assert(_debugLifecycleState == _ElementLifecycle.active);
assert(widget != null); assert(_widget != null); // Use the private property to avoid a CastError during hot reload.
assert(depth != null); assert(depth != null);
assert(_active); assert(_active);
if (_dependencies != null && _dependencies.isNotEmpty) { if (_dependencies != null && _dependencies.isNotEmpty) {
...@@ -3467,10 +3528,11 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3467,10 +3528,11 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
@mustCallSuper @mustCallSuper
void unmount() { void unmount() {
assert(_debugLifecycleState == _ElementLifecycle.inactive); assert(_debugLifecycleState == _ElementLifecycle.inactive);
assert(widget != null); assert(_widget != null); // Use the private property to avoid a CastError during hot reload.
assert(depth != null); assert(depth != null);
assert(!_active); assert(!_active);
final Key key = widget.key; // Use the private property to avoid a CastError during hot reload.
final Key key = _widget.key;
if (key is GlobalKey) { if (key is GlobalKey) {
key._unregister(this); key._unregister(this);
} }
......
// 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:file/file.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import '../src/common.dart';
import 'test_data/stateless_stateful_project.dart';
import 'test_driver.dart';
import 'test_utils.dart';
// This test verifies that we can hot reload a stateless widget into a
// stateful one and back.
void main() {
Directory tempDir;
final HotReloadProject _project = HotReloadProject();
FlutterRunTestDriver _flutter;
setUp(() async {
tempDir = createResolvedTempDirectorySync('hot_reload_test.');
await _project.setUpIn(tempDir);
_flutter = FlutterRunTestDriver(tempDir);
});
tearDown(() async {
await _flutter?.stop();
tryToDelete(tempDir);
});
test('Can switch between stateless and stateful', () async {
await _flutter.run();
await _flutter.hotReload();
final StringBuffer stdout = StringBuffer();
final StreamSubscription<String> subscription = _flutter.stdout.listen(stdout.writeln);
// switch to stateful.
_project.toggleState();
await _flutter.hotReload();
// switch to stateless.
_project.toggleState();
await _flutter.hotReload();
final String logs = stdout.toString();
expect(logs, contains('STATELESS'));
expect(logs, contains('STATEFUL'));
await subscription.cancel();
});
}
// 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_tools/src/globals.dart' as globals;
import '../test_utils.dart';
import 'project.dart';
class HotReloadProject extends Project {
@override
final String pubspec = '''
name: test
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
''';
@override
final String main = getCode(false);
static String getCode(bool stateful) {
return '''
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final ByteData message = const StringCodec().encodeMessage('AppLifecycleState.resumed');
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/lifecycle', message, (_) { });
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Child(),
);
}
}
class ${stateful ? 'Other' : 'Child'} extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('STATELESS');
return Container();
}
}
class ${stateful ? 'Child' : 'Other'} extends StatefulWidget {
State createState() => _State();
}
class _State extends State<${stateful ? 'Child' : 'Other'}>{
@override
Widget build(BuildContext context) {
print('STATEFUL');
return Container();
}
}
''';
}
/// Whether the template is currently stateful.
bool stateful = false;
void toggleState() {
stateful = !stateful;
writeFile(globals.fs.path.join(dir.path, 'lib', 'main.dart'), getCode(stateful));
}
}
...@@ -22,7 +22,8 @@ Directory createResolvedTempDirectorySync(String prefix) { ...@@ -22,7 +22,8 @@ Directory createResolvedTempDirectorySync(String prefix) {
void writeFile(String path, String content) { void writeFile(String path, String content) {
globals.fs.file(path) globals.fs.file(path)
..createSync(recursive: true) ..createSync(recursive: true)
..writeAsStringSync(content); ..writeAsStringSync(content)
..setLastModifiedSync(DateTime.now().add(const Duration(seconds: 10)));
} }
void writePackages(String folder) { void writePackages(String folder) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment