mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Improve exception reporting in Widgets framework
This specifically improves the reporting of exceptions in syncChild(), and makes the way we've been adding information to toStringName() less ad-hoc and easier to extend.
This commit is contained in:
parent
6413e58cb6
commit
c596d4c141
@ -140,7 +140,6 @@ abstract class GlobalKey extends Key {
|
||||
if (_syncedKeys.isEmpty && _removedKeys.isEmpty)
|
||||
return;
|
||||
try {
|
||||
|
||||
for (GlobalKey key in _syncedKeys) {
|
||||
Widget widget = _registry[key];
|
||||
if (widget != null && _syncListeners.containsKey(key)) {
|
||||
@ -149,7 +148,6 @@ abstract class GlobalKey extends Key {
|
||||
listener(key, widget);
|
||||
}
|
||||
}
|
||||
|
||||
for (GlobalKey key in _removedKeys) {
|
||||
if (!_registry.containsKey(key) && _removeListeners.containsKey(key)) {
|
||||
Set<GlobalKeyRemoveListener> localListeners = new Set<GlobalKeyRemoveListener>.from(_removeListeners[key]);
|
||||
@ -388,101 +386,113 @@ abstract class Widget {
|
||||
|
||||
// Returns the child which should be retained as the child of this node.
|
||||
Widget syncChild(Widget newNode, Widget oldNode, dynamic slot) {
|
||||
|
||||
if (newNode == oldNode) {
|
||||
// TODO(ianh): Simplify next few asserts once the analyzer is cleverer
|
||||
assert(newNode is! RenderObjectWrapper ||
|
||||
(newNode is RenderObjectWrapper && newNode._ancestor != null)); // if the child didn't change, it had better be configured
|
||||
assert(newNode is! StatefulComponent ||
|
||||
(newNode is StatefulComponent && newNode._isStateInitialized)); // if the child didn't change, it had better be configured
|
||||
if (newNode != null) {
|
||||
newNode.setParent(this);
|
||||
newNode._markAsFromCurrentGeneration();
|
||||
}
|
||||
return newNode; // Nothing to do. Subtrees must be identical.
|
||||
}
|
||||
|
||||
String debugDetails;
|
||||
assert(() {
|
||||
'You have probably used a single instance of a Widget in two different places in the widget tree. Widgets can only be used in one place at a time.';
|
||||
return newNode == null || newNode.isFromOldGeneration;
|
||||
// we save this information early because by the time the exception fires we might have changed everything around
|
||||
debugDetails = " old child: ${oldNode?.toStringName()}\n new child: ${newNode?.toStringName()}";
|
||||
return true;
|
||||
});
|
||||
try {
|
||||
|
||||
if (oldNode != null && !oldNode.isFromOldGeneration)
|
||||
oldNode = null;
|
||||
|
||||
if (newNode == null) {
|
||||
// the child in this slot has gone away
|
||||
// remove it if they old one is still here
|
||||
if (oldNode != null) {
|
||||
assert(oldNode != null);
|
||||
assert(oldNode.isFromOldGeneration);
|
||||
assert(oldNode.mounted);
|
||||
oldNode.detachRenderObject();
|
||||
oldNode.remove();
|
||||
assert(!oldNode.mounted);
|
||||
// we don't update the generation of oldNode, because there's
|
||||
// still a chance it could be reused as-is later in the tree.
|
||||
if (newNode == oldNode) {
|
||||
// TODO(ianh): Simplify next few asserts once the analyzer is cleverer
|
||||
assert(newNode is! RenderObjectWrapper ||
|
||||
(newNode is RenderObjectWrapper && newNode._ancestor != null)); // if the child didn't change, it had better be configured
|
||||
assert(newNode is! StatefulComponent ||
|
||||
(newNode is StatefulComponent && newNode._isStateInitialized)); // if the child didn't change, it had better be configured
|
||||
if (newNode != null) {
|
||||
newNode.setParent(this);
|
||||
newNode._markAsFromCurrentGeneration();
|
||||
}
|
||||
return newNode; // Nothing to do. Subtrees must be identical.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (oldNode != null) {
|
||||
assert(newNode != null);
|
||||
assert(newNode.isFromOldGeneration);
|
||||
assert(oldNode.isFromOldGeneration);
|
||||
if (!Widget._canSync(newNode, oldNode)) {
|
||||
assert(oldNode.mounted);
|
||||
// We want to handle the case where there is a removal of zero
|
||||
// or more widgets. In this case, we should be able to sync
|
||||
// ourselves with a Widget that is a descendant of the
|
||||
// oldNode, skipping the nodes in between. Let's try that.
|
||||
Widget deadNode = oldNode;
|
||||
Widget candidate = _getCandidateSingleChildFrom(oldNode);
|
||||
assert(() {
|
||||
'You have probably used a single instance of a Widget in two different places in the widget tree. Widgets can only be used in one place at a time.';
|
||||
return newNode == null || newNode.isFromOldGeneration;
|
||||
});
|
||||
|
||||
if (oldNode != null && !oldNode.isFromOldGeneration)
|
||||
oldNode = null;
|
||||
|
||||
while (candidate != null) {
|
||||
if (Widget._canSync(newNode, candidate)) {
|
||||
assert(candidate.parent != null);
|
||||
assert(candidate.parent.singleChild == candidate);
|
||||
if (candidate.renderObject != deadNode.renderObject) {
|
||||
// TODO(ianh): Handle removal across RenderNode boundaries
|
||||
} else {
|
||||
candidate.parent.takeChild();
|
||||
oldNode = candidate;
|
||||
}
|
||||
break;
|
||||
}
|
||||
candidate = _getCandidateSingleChildFrom(candidate);
|
||||
if (newNode == null) {
|
||||
// the child in this slot has gone away
|
||||
// remove it if they old one is still here
|
||||
if (oldNode != null) {
|
||||
assert(oldNode != null);
|
||||
assert(oldNode.isFromOldGeneration);
|
||||
assert(oldNode.mounted);
|
||||
oldNode.detachRenderObject();
|
||||
oldNode.remove();
|
||||
assert(!oldNode.mounted);
|
||||
// we don't update the generation of oldNode, because there's
|
||||
// still a chance it could be reused as-is later in the tree.
|
||||
}
|
||||
|
||||
// TODO(ianh): Handle insertion, too...
|
||||
|
||||
if (oldNode == null)
|
||||
deadNode.detachRenderObject();
|
||||
deadNode.remove();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (oldNode != null) {
|
||||
assert(newNode != null);
|
||||
assert(newNode.isFromOldGeneration);
|
||||
assert(oldNode.isFromOldGeneration);
|
||||
assert(Widget._canSync(newNode, oldNode));
|
||||
if (oldNode.retainStatefulNodeIfPossible(newNode)) {
|
||||
if (!Widget._canSync(newNode, oldNode)) {
|
||||
assert(oldNode.mounted);
|
||||
assert(!newNode.mounted);
|
||||
oldNode.setParent(this);
|
||||
oldNode._sync(newNode, slot);
|
||||
assert(oldNode.renderObject is RenderObject);
|
||||
return oldNode;
|
||||
} else {
|
||||
oldNode.setParent(null);
|
||||
// We want to handle the case where there is a removal of zero
|
||||
// or more widgets. In this case, we should be able to sync
|
||||
// ourselves with a Widget that is a descendant of the
|
||||
// oldNode, skipping the nodes in between. Let's try that.
|
||||
Widget deadNode = oldNode;
|
||||
Widget candidate = _getCandidateSingleChildFrom(oldNode);
|
||||
oldNode = null;
|
||||
|
||||
while (candidate != null) {
|
||||
if (Widget._canSync(newNode, candidate)) {
|
||||
assert(candidate.parent != null);
|
||||
assert(candidate.parent.singleChild == candidate);
|
||||
if (candidate.renderObject != deadNode.renderObject) {
|
||||
// TODO(ianh): Handle removal across RenderNode boundaries
|
||||
} else {
|
||||
candidate.parent.takeChild();
|
||||
oldNode = candidate;
|
||||
}
|
||||
break;
|
||||
}
|
||||
candidate = _getCandidateSingleChildFrom(candidate);
|
||||
}
|
||||
|
||||
// TODO(ianh): Handle insertion, too...
|
||||
|
||||
if (oldNode == null)
|
||||
deadNode.detachRenderObject();
|
||||
deadNode.remove();
|
||||
}
|
||||
if (oldNode != null) {
|
||||
assert(newNode.isFromOldGeneration);
|
||||
assert(oldNode.isFromOldGeneration);
|
||||
assert(Widget._canSync(newNode, oldNode));
|
||||
if (oldNode.retainStatefulNodeIfPossible(newNode)) {
|
||||
assert(oldNode.mounted);
|
||||
assert(!newNode.mounted);
|
||||
oldNode.setParent(this);
|
||||
oldNode._sync(newNode, slot);
|
||||
assert(oldNode.renderObject is RenderObject);
|
||||
return oldNode;
|
||||
} else {
|
||||
oldNode.setParent(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert(oldNode == null || (oldNode.mounted == false && oldNode.parent == null));
|
||||
newNode.setParent(this);
|
||||
newNode._sync(oldNode, slot);
|
||||
assert(newNode.renderObject is RenderObject);
|
||||
return newNode;
|
||||
assert(oldNode == null || (oldNode.mounted == false && oldNode.parent == null));
|
||||
newNode.setParent(this);
|
||||
newNode._sync(oldNode, slot);
|
||||
assert(newNode.renderObject is RenderObject);
|
||||
return newNode;
|
||||
|
||||
} catch (e, stack) {
|
||||
_debugReportException('syncing children of ${this.toStringName()}\n$debugDetails', e, stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _adjustPrefixWithParentCheck(Widget child, String prefix) {
|
||||
@ -507,19 +517,25 @@ abstract class Widget {
|
||||
nextPrefix = prefix + ' ';
|
||||
childrenString += lastChild.toString(nextPrefix, _adjustPrefixWithParentCheck(lastChild, nextStartPrefix));
|
||||
}
|
||||
String suffix = '';
|
||||
return '$startPrefix${toStringName()}\n$childrenString';
|
||||
}
|
||||
String toStringName() {
|
||||
List<String> details = <String>[];
|
||||
debugAddDetails(details);
|
||||
String detailString = details.join('; ');
|
||||
return '$runtimeType($detailString})';
|
||||
}
|
||||
void debugAddDetails(List<String> details) {
|
||||
if (key != null)
|
||||
details.add('$key');
|
||||
details.add('hashCode=$hashCode');
|
||||
details.add(mounted ? 'mounted' : 'not mounted');
|
||||
String generationString = '';
|
||||
if (_generation != _currentGeneration) {
|
||||
int delta = _generation - _currentGeneration;
|
||||
String sign = delta < 0 ? '' : '+';
|
||||
suffix = ' gen$sign$delta';
|
||||
details.add('gen$sign$delta');
|
||||
}
|
||||
return '$startPrefix${toStringName()}$suffix\n$childrenString';
|
||||
}
|
||||
String toStringName() {
|
||||
String keyString = key == null ? '' : '$key; ';
|
||||
String hashCodeString = 'hashCode=$hashCode';
|
||||
String mountedString = mounted ? '; mounted' : '; not mounted';
|
||||
return '$runtimeType($keyString$hashCodeString$mountedString)';
|
||||
}
|
||||
|
||||
// This function can be safely called when the layout is valid.
|
||||
@ -902,6 +918,11 @@ abstract class Component extends Widget {
|
||||
|
||||
Widget build();
|
||||
|
||||
void debugAddDetails(List<String> details) {
|
||||
super.debugAddDetails(details);
|
||||
if (_dirty)
|
||||
details.add('dirty');
|
||||
}
|
||||
}
|
||||
|
||||
abstract class StatefulComponent extends Component {
|
||||
@ -964,10 +985,9 @@ abstract class StatefulComponent extends Component {
|
||||
_scheduleBuild();
|
||||
}
|
||||
|
||||
String toStringName() {
|
||||
if (_isStateInitialized)
|
||||
return 'Stateful ${super.toStringName()}';
|
||||
return 'Stateless ${super.toStringName()}';
|
||||
void debugAddDetails(List<String> details) {
|
||||
super.debugAddDetails(details);
|
||||
details.add(_isStateInitialized ? 'stateful' : 'stateless');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user