Unverified Commit 38808d9f authored by chunhtai's avatar chunhtai Committed by GitHub

Reland fix 25807 implement move for sliver multibox widget (#31978)

parent 39d660be
...@@ -1125,6 +1125,7 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -1125,6 +1125,7 @@ class _TabBarViewState extends State<TabBarView> {
TabController _controller; TabController _controller;
PageController _pageController; PageController _pageController;
List<Widget> _children; List<Widget> _children;
List<Widget> _childrenWithKey;
int _currentIndex; int _currentIndex;
int _warpUnderwayCount = 0; int _warpUnderwayCount = 0;
...@@ -1156,7 +1157,7 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -1156,7 +1157,7 @@ class _TabBarViewState extends State<TabBarView> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_children = widget.children; _updateChildren();
} }
@override @override
...@@ -1173,7 +1174,7 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -1173,7 +1174,7 @@ class _TabBarViewState extends State<TabBarView> {
if (widget.controller != oldWidget.controller) if (widget.controller != oldWidget.controller)
_updateTabController(); _updateTabController();
if (widget.children != oldWidget.children && _warpUnderwayCount == 0) if (widget.children != oldWidget.children && _warpUnderwayCount == 0)
_children = widget.children; _updateChildren();
} }
@override @override
...@@ -1184,6 +1185,11 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -1184,6 +1185,11 @@ class _TabBarViewState extends State<TabBarView> {
super.dispose(); super.dispose();
} }
void _updateChildren() {
_children = widget.children;
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
}
void _handleTabControllerAnimationTick() { void _handleTabControllerAnimationTick() {
if (_warpUnderwayCount > 0 || !_controller.indexIsChanging) if (_warpUnderwayCount > 0 || !_controller.indexIsChanging)
return; // This widget is driving the controller's animation. return; // This widget is driving the controller's animation.
...@@ -1206,28 +1212,30 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -1206,28 +1212,30 @@ class _TabBarViewState extends State<TabBarView> {
return _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease); return _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
assert((_currentIndex - previousIndex).abs() > 1); assert((_currentIndex - previousIndex).abs() > 1);
int initialPage; final int initialPage = _currentIndex > previousIndex
? _currentIndex - 1
: _currentIndex + 1;
final List<Widget> originalChildren = _childrenWithKey;
setState(() { setState(() {
_warpUnderwayCount += 1; _warpUnderwayCount += 1;
_children = List<Widget>.from(widget.children, growable: false);
if (_currentIndex > previousIndex) {
_children[_currentIndex - 1] = _children[previousIndex];
initialPage = _currentIndex - 1;
} else {
_children[_currentIndex + 1] = _children[previousIndex];
initialPage = _currentIndex + 1;
}
});
_childrenWithKey = List<Widget>.from(_childrenWithKey, growable: false);
final Widget temp = _childrenWithKey[initialPage];
_childrenWithKey[initialPage] = _childrenWithKey[previousIndex];
_childrenWithKey[previousIndex] = temp;
});
_pageController.jumpToPage(initialPage); _pageController.jumpToPage(initialPage);
await _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease); await _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
if (!mounted) if (!mounted)
return Future<void>.value(); return Future<void>.value();
setState(() { setState(() {
_warpUnderwayCount -= 1; _warpUnderwayCount -= 1;
_children = widget.children; if (widget.children != _children) {
_updateChildren();
} else {
_childrenWithKey = originalChildren;
}
}); });
} }
...@@ -1272,7 +1280,7 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -1272,7 +1280,7 @@ class _TabBarViewState extends State<TabBarView> {
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
controller: _pageController, controller: _pageController,
physics: widget.physics == null ? _kTabBarViewPhysics : _kTabBarViewPhysics.applyTo(widget.physics), physics: widget.physics == null ? _kTabBarViewPhysics : _kTabBarViewPhysics.applyTo(widget.physics),
children: _children, children: _childrenWithKey,
), ),
); );
} }
......
...@@ -85,7 +85,8 @@ abstract class RenderSliverBoxChildManager { ...@@ -85,7 +85,8 @@ abstract class RenderSliverBoxChildManager {
/// list). /// list).
int get childCount; int get childCount;
/// Called during [RenderSliverMultiBoxAdaptor.adoptChild]. /// Called during [RenderSliverMultiBoxAdaptor.adoptChild] or
/// [RenderSliverMultiBoxAdaptor.move].
/// ///
/// Subclasses must ensure that the [SliverMultiBoxAdaptorParentData.index] /// Subclasses must ensure that the [SliverMultiBoxAdaptorParentData.index]
/// field of the child's [RenderObject.parentData] accurately reflects the /// field of the child's [RenderObject.parentData] accurately reflects the
...@@ -193,7 +194,12 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...@@ -193,7 +194,12 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
RenderSliverMultiBoxAdaptor({ RenderSliverMultiBoxAdaptor({
@required RenderSliverBoxChildManager childManager, @required RenderSliverBoxChildManager childManager,
}) : assert(childManager != null), }) : assert(childManager != null),
_childManager = childManager; _childManager = childManager {
assert(() {
_debugDanglingKeepAlives = <RenderBox>[];
return true;
}());
}
@override @override
void setupParentData(RenderObject child) { void setupParentData(RenderObject child) {
...@@ -214,6 +220,27 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...@@ -214,6 +220,27 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
/// The nodes being kept alive despite not being visible. /// The nodes being kept alive despite not being visible.
final Map<int, RenderBox> _keepAliveBucket = <int, RenderBox>{}; final Map<int, RenderBox> _keepAliveBucket = <int, RenderBox>{};
List<RenderBox> _debugDanglingKeepAlives;
/// Indicates whether integrity check is enabled.
///
/// Setting this property to true will immediately perform an integrity check.
///
/// The integrity check consists of:
///
/// 1. Verify that the children index in childList is in ascending order.
/// 2. Verify that there is no dangling keepalive child as the result of [move].
bool get debugChildIntegrityEnabled => _debugChildIntegrityEnabled;
bool _debugChildIntegrityEnabled = true;
set debugChildIntegrityEnabled(bool enabled) {
assert(enabled != null);
assert(() {
_debugChildIntegrityEnabled = enabled;
return _debugVerifyChildOrder() &&
(!_debugChildIntegrityEnabled || _debugDanglingKeepAlives.isEmpty);
}());
}
@override @override
void adoptChild(RenderObject child) { void adoptChild(RenderObject child) {
super.adoptChild(child); super.adoptChild(child);
...@@ -224,21 +251,70 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...@@ -224,21 +251,70 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
bool _debugAssertChildListLocked() => childManager.debugAssertChildListLocked(); bool _debugAssertChildListLocked() => childManager.debugAssertChildListLocked();
/// Verify that the child list index is in strictly increasing order.
///
/// This has no effect in release builds.
bool _debugVerifyChildOrder(){
if (_debugChildIntegrityEnabled) {
RenderBox child = firstChild;
int index;
while (child != null) {
index = indexOf(child);
child = childAfter(child);
assert(child == null || indexOf(child) > index);
}
}
return true;
}
@override @override
void insert(RenderBox child, { RenderBox after }) { void insert(RenderBox child, { RenderBox after }) {
assert(!_keepAliveBucket.containsValue(child)); assert(!_keepAliveBucket.containsValue(child));
super.insert(child, after: after); super.insert(child, after: after);
assert(firstChild != null); assert(firstChild != null);
assert(() { assert(_debugVerifyChildOrder());
int index = indexOf(firstChild); }
RenderBox child = childAfter(firstChild);
while (child != null) { @override
assert(indexOf(child) > index); void move(RenderBox child, { RenderBox after }) {
index = indexOf(child); // There are two scenarios:
child = childAfter(child); //
// 1. The child is not keptAlive.
// The child is in the childList maintained by ContainerRenderObjectMixin.
// We can call super.move and update parentData with the new slot.
//
// 2. The child is keptAlive.
// In this case, the child is no longer in the childList but might be stored in
// [_keepAliveBucket]. We need to update the location of the child in the bucket.
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
if (!childParentData.keptAlive) {
super.move(child, after: after);
childManager.didAdoptChild(child); // updates the slot in the parentData
// Its slot may change even if super.move does not change the position.
// In this case, we still want to mark as needs layout.
markNeedsLayout();
} else {
// If the child in the bucket is not current child, that means someone has
// already moved and replaced current child, and we cannot remove this child.
if (_keepAliveBucket[childParentData.index] == child) {
_keepAliveBucket.remove(childParentData.index);
} }
assert(() {
_debugDanglingKeepAlives.remove(child);
return true;
}());
// Update the slot and reinsert back to _keepAliveBucket in the new slot.
childManager.didAdoptChild(child);
// If there is an existing child in the new slot, that mean that child will
// be moved to other index. In other cases, the existing child should have been
// removed by updateChild. Thus, it is ok to overwrite it.
assert(() {
if (_keepAliveBucket.containsKey(childParentData.index))
_debugDanglingKeepAlives.add(_keepAliveBucket[childParentData.index]);
return true; return true;
}()); }());
_keepAliveBucket[childParentData.index] = child;
}
} }
@override @override
...@@ -249,6 +325,10 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...@@ -249,6 +325,10 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
return; return;
} }
assert(_keepAliveBucket[childParentData.index] == child); assert(_keepAliveBucket[childParentData.index] == child);
assert(() {
_debugDanglingKeepAlives.remove(child);
return true;
}());
_keepAliveBucket.remove(childParentData.index); _keepAliveBucket.remove(childParentData.index);
dropChild(child); dropChild(child);
} }
......
...@@ -149,6 +149,13 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key { ...@@ -149,6 +149,13 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
assert(() { assert(() {
assert(parent != null); assert(parent != null);
if (_debugReservations.containsKey(this) && _debugReservations[this] != parent) { if (_debugReservations.containsKey(this) && _debugReservations[this] != parent) {
// Reserving a new parent while the old parent is not attached is ok.
// This can happen when a renderObject detaches and re-attaches to rendering
// tree multiple times.
if (_debugReservations[this].renderObject?.attached == false) {
_debugReservations[this] = parent;
return true;
}
// It's possible for an element to get built multiple times in one // It's possible for an element to get built multiple times in one
// frame, in which case it'll reserve the same child's key multiple // frame, in which case it'll reserve the same child's key multiple
// times. We catch multiple children of one widget having the same key // times. We catch multiple children of one widget having the same key
......
...@@ -174,6 +174,13 @@ abstract class SliverChildDelegate { ...@@ -174,6 +174,13 @@ abstract class SliverChildDelegate {
/// away. /// away.
bool shouldRebuild(covariant SliverChildDelegate oldDelegate); bool shouldRebuild(covariant SliverChildDelegate oldDelegate);
/// Find index of child element with associated key.
///
/// This will be called during [performRebuild] in [SliverMultiBoxAdaptorElement]
/// to check if a child has moved to a different position. It should return the
/// index of the child element with associated key, null if not found.
int findIndexByKey(Key key) => null;
@override @override
String toString() { String toString() {
final List<String> description = <String>[]; final List<String> description = <String>[];
...@@ -195,6 +202,12 @@ abstract class SliverChildDelegate { ...@@ -195,6 +202,12 @@ abstract class SliverChildDelegate {
} }
} }
class _SaltedValueKey extends ValueKey<Key>{
const _SaltedValueKey(Key key): assert(key != null), super(key);
}
typedef ChildIndexGetter = int Function(Key key);
/// A delegate that supplies children for slivers using a builder callback. /// A delegate that supplies children for slivers using a builder callback.
/// ///
/// Many slivers lazily construct their box children to avoid creating more /// Many slivers lazily construct their box children to avoid creating more
...@@ -306,8 +319,14 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -306,8 +319,14 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// The [builder], [addAutomaticKeepAlives], [addRepaintBoundaries], /// The [builder], [addAutomaticKeepAlives], [addRepaintBoundaries],
/// [addSemanticIndexes], and [semanticIndexCallback] arguments must not be /// [addSemanticIndexes], and [semanticIndexCallback] arguments must not be
/// null. /// null.
///
/// If the order in which [builder] returns children ever changes, consider
/// providing a [findChildIndex]. This allows the delegate to find the new index
/// for a child that was previously located at a different index to attach the
/// existing state to the [Widget] at its new location.
const SliverChildBuilderDelegate( const SliverChildBuilderDelegate(
this.builder, { this.builder, {
this.findChildIndexCallback,
this.childCount, this.childCount,
this.addAutomaticKeepAlives = true, this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true, this.addRepaintBoundaries = true,
...@@ -388,6 +407,31 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -388,6 +407,31 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// Defaults to providing an index for each widget. /// Defaults to providing an index for each widget.
final SemanticIndexCallback semanticIndexCallback; final SemanticIndexCallback semanticIndexCallback;
/// Called to find the new index of a child based on its key in case of reordering.
///
/// If not provided, a child widget may not map to its existing [RenderObject]
/// when the order in which children are returned from [builder] changes.
/// This may result in state-loss.
///
/// This callback should take an input [Key], and It should return the
/// index of the child element with associated key, null if not found.
final ChildIndexGetter findChildIndexCallback;
@override
int findIndexByKey(Key key) {
if (findChildIndexCallback == null)
return null;
assert(key != null);
Key childKey;
if (key is _SaltedValueKey) {
final _SaltedValueKey saltedValueKey = key;
childKey = saltedValueKey.value;
} else {
childKey = key;
}
return findChildIndexCallback(childKey);
}
@override @override
Widget build(BuildContext context, int index) { Widget build(BuildContext context, int index) {
assert(builder != null); assert(builder != null);
...@@ -401,8 +445,9 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -401,8 +445,9 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
} }
if (child == null) if (child == null)
return null; return null;
final Key key = child.key != null ? _SaltedValueKey(child.key) : null;
if (addRepaintBoundaries) if (addRepaintBoundaries)
child = RepaintBoundary.wrap(child, index); child = RepaintBoundary(child: child);
if (addSemanticIndexes) { if (addSemanticIndexes) {
final int semanticIndex = semanticIndexCallback(child, index); final int semanticIndex = semanticIndexCallback(child, index);
if (semanticIndex != null) if (semanticIndex != null)
...@@ -410,7 +455,7 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -410,7 +455,7 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
} }
if (addAutomaticKeepAlives) if (addAutomaticKeepAlives)
child = AutomaticKeepAlive(child: child); child = AutomaticKeepAlive(child: child);
return child; return KeyedSubtree(child: child, key: key);
} }
@override @override
...@@ -478,7 +523,10 @@ class SliverChildListDelegate extends SliverChildDelegate { ...@@ -478,7 +523,10 @@ class SliverChildListDelegate extends SliverChildDelegate {
/// The [children], [addAutomaticKeepAlives], [addRepaintBoundaries], /// The [children], [addAutomaticKeepAlives], [addRepaintBoundaries],
/// [addSemanticIndexes], and [semanticIndexCallback] arguments must not be /// [addSemanticIndexes], and [semanticIndexCallback] arguments must not be
/// null. /// null.
const SliverChildListDelegate( ///
/// If the order of children` never changes, consider using the constant
/// [SliverChildListDelegate.fixed] constructor.
SliverChildListDelegate(
this.children, { this.children, {
this.addAutomaticKeepAlives = true, this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true, this.addRepaintBoundaries = true,
...@@ -489,7 +537,31 @@ class SliverChildListDelegate extends SliverChildDelegate { ...@@ -489,7 +537,31 @@ class SliverChildListDelegate extends SliverChildDelegate {
assert(addAutomaticKeepAlives != null), assert(addAutomaticKeepAlives != null),
assert(addRepaintBoundaries != null), assert(addRepaintBoundaries != null),
assert(addSemanticIndexes != null), assert(addSemanticIndexes != null),
assert(semanticIndexCallback != null); assert(semanticIndexCallback != null),
_keyToIndex = <Key, int>{null: 0};
/// Creates a constant version of the delegate that supplies children for
/// slivers using the given list.
///
/// If the order of the children will change, consider using the regular
/// [SliverChildListDelegate] constructor.
///
/// The [children], [addAutomaticKeepAlives], [addRepaintBoundaries],
/// [addSemanticIndexes], and [semanticIndexCallback] arguments must not be
/// null.
const SliverChildListDelegate.fixed(
this.children, {
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
}) : assert(children != null),
assert(addAutomaticKeepAlives != null),
assert(addRepaintBoundaries != null),
assert(addSemanticIndexes != null),
assert(semanticIndexCallback != null),
_keyToIndex = null;
/// Whether to wrap each child in an [AutomaticKeepAlive]. /// Whether to wrap each child in an [AutomaticKeepAlive].
/// ///
...@@ -544,15 +616,63 @@ class SliverChildListDelegate extends SliverChildDelegate { ...@@ -544,15 +616,63 @@ class SliverChildListDelegate extends SliverChildDelegate {
/// The widgets to display. /// The widgets to display.
final List<Widget> children; final List<Widget> children;
/// A map to cache key to index lookup for children.
///
/// _keyToIndex[null] is used as current index during the lazy loading process
/// in [_findChildIndex]. _keyToIndex should never be used for looking up null key.
final Map<Key, int> _keyToIndex;
bool get _isConstantInstance => _keyToIndex == null;
int _findChildIndex(Key key) {
if (_isConstantInstance) {
return null;
}
// Lazily fill the [_keyToIndex].
if (!_keyToIndex.containsKey(key)) {
int index = _keyToIndex[null];
while (index < children.length) {
final Widget child = children[index];
if (child.key != null) {
_keyToIndex[child.key] = index;
}
if (child.key == key) {
// Record current index for next function call.
_keyToIndex[null] = index + 1;
return index;
}
index += 1;
}
_keyToIndex[null] = index;
} else {
return _keyToIndex[key];
}
return null;
}
@override
int findIndexByKey(Key key) {
assert(key != null);
Key childKey;
if (key is _SaltedValueKey) {
final _SaltedValueKey saltedValueKey = key;
childKey = saltedValueKey.value;
} else {
childKey = key;
}
return _findChildIndex(childKey);
}
@override @override
Widget build(BuildContext context, int index) { Widget build(BuildContext context, int index) {
assert(children != null); assert(children != null);
if (index < 0 || index >= children.length) if (index < 0 || index >= children.length)
return null; return null;
Widget child = children[index]; Widget child = children[index];
final Key key = child.key != null? _SaltedValueKey(child.key) : null;
assert(child != null); assert(child != null);
if (addRepaintBoundaries) if (addRepaintBoundaries)
child = RepaintBoundary.wrap(child, index); child = RepaintBoundary(child: child);
if (addSemanticIndexes) { if (addSemanticIndexes) {
final int semanticIndex = semanticIndexCallback(child, index); final int semanticIndex = semanticIndexCallback(child, index);
if (semanticIndex != null) if (semanticIndex != null)
...@@ -560,7 +680,7 @@ class SliverChildListDelegate extends SliverChildDelegate { ...@@ -560,7 +680,7 @@ class SliverChildListDelegate extends SliverChildDelegate {
} }
if (addAutomaticKeepAlives) if (addAutomaticKeepAlives)
child = AutomaticKeepAlive(child: child); child = AutomaticKeepAlive(child: child);
return child; return KeyedSubtree(child: child, key: key);
} }
@override @override
...@@ -979,9 +1099,15 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render ...@@ -979,9 +1099,15 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
_currentBeforeChild = null; _currentBeforeChild = null;
assert(_currentlyUpdatingChildIndex == null); assert(_currentlyUpdatingChildIndex == null);
try { try {
final SplayTreeMap<int, Element> newChildren = SplayTreeMap<int, Element>();
void processElement(int index) { void processElement(int index) {
_currentlyUpdatingChildIndex = index; _currentlyUpdatingChildIndex = index;
final Element newChild = updateChild(_childElements[index], _build(index), index); if (_childElements[index] != null && _childElements[index] != newChildren[index]) {
// This index has an old child that isn't used anywhere and should be deactivated.
_childElements[index] = updateChild(_childElements[index], null, index);
}
final Element newChild = updateChild(newChildren[index], _build(index), index);
if (newChild != null) { if (newChild != null) {
_childElements[index] = newChild; _childElements[index] = newChild;
final SliverMultiBoxAdaptorParentData parentData = newChild.renderObject.parentData; final SliverMultiBoxAdaptorParentData parentData = newChild.renderObject.parentData;
...@@ -991,14 +1117,32 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render ...@@ -991,14 +1117,32 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
_childElements.remove(index); _childElements.remove(index);
} }
} }
// processElement may modify the Map - need to do a .toList() here.
_childElements.keys.toList().forEach(processElement); for (int index in _childElements.keys.toList()) {
final Key key = _childElements[index].widget.key;
final int newIndex = key == null ? null : widget.delegate.findIndexByKey(key);
if (newIndex != null && newIndex != index) {
newChildren[newIndex] = _childElements[index];
// We need to make sure the original index gets processed.
newChildren.putIfAbsent(index, () => null);
// We do not want the remapped child to get deactivated during processElement.
_childElements.remove(index);
} else {
newChildren.putIfAbsent(index, () => _childElements[index]);
}
}
renderObject.debugChildIntegrityEnabled = false; // Moving children will temporary violate the integrity.
newChildren.keys.forEach(processElement);
if (_didUnderflow) { if (_didUnderflow) {
final int lastKey = _childElements.lastKey() ?? -1; final int lastKey = _childElements.lastKey() ?? -1;
processElement(lastKey + 1); final int rightBoundary = lastKey + 1;
newChildren[rightBoundary] = _childElements[rightBoundary];
processElement(rightBoundary);
} }
} finally { } finally {
_currentlyUpdatingChildIndex = null; _currentlyUpdatingChildIndex = null;
renderObject.debugChildIntegrityEnabled = true;
} }
} }
...@@ -1038,7 +1182,6 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render ...@@ -1038,7 +1182,6 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
if (oldParentData != newParentData && oldParentData != null && newParentData != null) { if (oldParentData != newParentData && oldParentData != null && newParentData != null) {
newParentData.layoutOffset = oldParentData.layoutOffset; newParentData.layoutOffset = oldParentData.layoutOffset;
} }
return newChild; return newChild;
} }
...@@ -1163,9 +1306,9 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render ...@@ -1163,9 +1306,9 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
@override @override
void moveChildRenderObject(covariant RenderObject child, int slot) { void moveChildRenderObject(covariant RenderObject child, int slot) {
// TODO(ianh): At some point we should be better about noticing when a assert(slot != null);
// particular LocalKey changes slot, and handle moving the nodes around. assert(_currentlyUpdatingChildIndex == slot);
assert(false); renderObject.move(child, after: _currentBeforeChild);
} }
@override @override
......
...@@ -49,6 +49,7 @@ class StateMarkerState extends State<StateMarker> { ...@@ -49,6 +49,7 @@ class StateMarkerState extends State<StateMarker> {
} }
class AlwaysKeepAliveWidget extends StatefulWidget { class AlwaysKeepAliveWidget extends StatefulWidget {
const AlwaysKeepAliveWidget({ Key key}) : super(key: key);
static String text = 'AlwaysKeepAlive'; static String text = 'AlwaysKeepAlive';
@override @override
AlwaysKeepAliveState createState() => AlwaysKeepAliveState(); AlwaysKeepAliveState createState() => AlwaysKeepAliveState();
...@@ -1987,6 +1988,56 @@ void main() { ...@@ -1987,6 +1988,56 @@ void main() {
}); });
testWidgets('Skipping tabs with global key does not crash', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/24660
final List<String> tabs = <String>[
'Tab1',
'Tab2',
'Tab3',
'Tab4',
];
final TabController controller = TabController(
vsync: const TestVSync(),
length: tabs.length,
);
await tester.pumpWidget(
MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 300.0,
height: 200.0,
child: Scaffold(
appBar: AppBar(
title: const Text('tabs'),
bottom: TabBar(
controller: controller,
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
),
),
body: TabBarView(
controller: controller,
children: <Widget>[
Text('1', key: GlobalKey()),
Text('2', key: GlobalKey()),
Text('3', key: GlobalKey()),
Text('4', key: GlobalKey()),
],
),
),
),
),
),
);
expect(find.text('1'), findsOneWidget);
expect(find.text('4'), findsNothing);
await tester.tap(find.text('Tab4'));
await tester.pumpAndSettle();
expect(controller.index, 3);
expect(find.text('4'), findsOneWidget);
expect(find.text('1'), findsNothing);
});
testWidgets('Skipping tabs with a KeepAlive child works', (WidgetTester tester) async { testWidgets('Skipping tabs with a KeepAlive child works', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/11895 // Regression test for https://github.com/flutter/flutter/issues/11895
final List<String> tabs = <String>[ final List<String> tabs = <String>[
...@@ -2018,7 +2069,7 @@ void main() { ...@@ -2018,7 +2069,7 @@ void main() {
body: TabBarView( body: TabBarView(
controller: controller, controller: controller,
children: <Widget>[ children: <Widget>[
AlwaysKeepAliveWidget(), AlwaysKeepAliveWidget(key: UniqueKey()),
const Text('2'), const Text('2'),
const Text('3'), const Text('3'),
const Text('4'), const Text('4'),
......
...@@ -9,10 +9,10 @@ import 'package:flutter/widgets.dart'; ...@@ -9,10 +9,10 @@ import 'package:flutter/widgets.dart';
void main() { void main() {
testWidgets('Sliver in a box', (WidgetTester tester) async { testWidgets('Sliver in a box', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const DecoratedBox( DecoratedBox(
decoration: BoxDecoration(), decoration: const BoxDecoration(),
child: SliverList( child: SliverList(
delegate: SliverChildListDelegate(<Widget>[]), delegate: SliverChildListDelegate(const <Widget>[]),
), ),
), ),
); );
...@@ -21,9 +21,9 @@ void main() { ...@@ -21,9 +21,9 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Row( Row(
children: const <Widget>[ children: <Widget>[
SliverList( SliverList(
delegate: SliverChildListDelegate(<Widget>[]), delegate: SliverChildListDelegate(const <Widget>[]),
), ),
], ],
), ),
......
...@@ -451,6 +451,41 @@ void main() { ...@@ -451,6 +451,41 @@ void main() {
expect(count, 2); expect(count, 2);
}); });
testWidgets('GlobalKey - dettach and re-attach child to different parents', (WidgetTester tester) async {
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Container(
height: 100,
child: CustomScrollView(
controller: ScrollController(),
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate(<Widget>[
Text('child', key: GlobalKey()),
]),
)
],
),
),
),
));
final SliverMultiBoxAdaptorElement element = tester.element(find.byType(SliverList));
Element childElement;
// Removing and recreating child with same Global Key should not trigger
// duplicate key error.
element.visitChildren((Element e) {
childElement = e;
});
element.removeChild(childElement.renderObject);
element.createChild(0, after: null);
element.visitChildren((Element e) {
childElement = e;
});
element.removeChild(childElement.renderObject);
element.createChild(0, after: null);
});
testWidgets('Defunct setState throws exception', (WidgetTester tester) async { testWidgets('Defunct setState throws exception', (WidgetTester tester) async {
StateSetter setState; StateSetter setState;
......
...@@ -22,12 +22,12 @@ void main() { ...@@ -22,12 +22,12 @@ void main() {
data: const MediaQueryData(), data: const MediaQueryData(),
child: CustomScrollView( child: CustomScrollView(
controller: controller, controller: controller,
slivers: const <Widget>[ slivers: <Widget>[
SliverAppBar(floating: true, pinned: true, expandedHeight: 200.0, title: Text('A')), const SliverAppBar(floating: true, pinned: true, expandedHeight: 200.0, title: Text('A')),
SliverAppBar(primary: false, pinned: true, title: Text('B')), const SliverAppBar(primary: false, pinned: true, title: Text('B')),
SliverList( SliverList(
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
<Widget>[ const <Widget>[
Text('C'), Text('C'),
Text('D'), Text('D'),
SizedBox(height: 500.0), SizedBox(height: 500.0),
......
...@@ -203,9 +203,9 @@ void main() { ...@@ -203,9 +203,9 @@ void main() {
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
slivers: <Widget>[ slivers: <Widget>[
SliverPersistentHeader(delegate: TestDelegate(), floating: true), SliverPersistentHeader(delegate: TestDelegate(), floating: true),
const SliverList( SliverList(
delegate: SliverChildListDelegate(<Widget>[ delegate: SliverChildListDelegate(<Widget>[
SizedBox( const SizedBox(
height: 300.0, height: 300.0,
child: Text('X'), child: Text('X'),
), ),
......
...@@ -258,9 +258,9 @@ void main() { ...@@ -258,9 +258,9 @@ void main() {
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
slivers: <Widget>[ slivers: <Widget>[
SliverPersistentHeader(delegate: TestDelegate(), pinned: true), SliverPersistentHeader(delegate: TestDelegate(), pinned: true),
const SliverList( SliverList(
delegate: SliverChildListDelegate(<Widget>[ delegate: SliverChildListDelegate(<Widget>[
SizedBox( const SizedBox(
height: 300.0, height: 300.0,
child: Text('X'), child: Text('X'),
), ),
......
...@@ -82,8 +82,40 @@ void main() { ...@@ -82,8 +82,40 @@ void main() {
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
slivers: <Widget>[ slivers: <Widget>[
SliverPersistentHeader(delegate: TestDelegate()), SliverPersistentHeader(delegate: TestDelegate()),
const SliverList( SliverList(
delegate: SliverChildListDelegate(<Widget>[ delegate: SliverChildListDelegate(<Widget>[
const SizedBox(
height: 300.0,
child: Text('X'),
),
]),
),
],
),
),
);
expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 200.0));
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
position.jumpTo(-50.0);
await tester.pump();
expect(tester.getTopLeft(find.byType(Container)), Offset.zero);
expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 250.0));
});
testWidgets('Sliver appbars const child delegate - scrolling - overscroll gap is below header', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
SliverPersistentHeader(delegate: TestDelegate()),
const SliverList(
delegate: SliverChildListDelegate.fixed(<Widget>[
SizedBox( SizedBox(
height: 300.0, height: 300.0,
child: Text('X'), child: Text('X'),
......
...@@ -9,6 +9,28 @@ import 'package:flutter/widgets.dart'; ...@@ -9,6 +9,28 @@ import 'package:flutter/widgets.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
Future<void> test(WidgetTester tester, double offset) { Future<void> test(WidgetTester tester, double offset) {
return tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Viewport(
offset: ViewportOffset.fixed(offset),
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate(const <Widget>[
SizedBox(height: 400.0, child: Text('a')),
SizedBox(height: 400.0, child: Text('b')),
SizedBox(height: 400.0, child: Text('c')),
SizedBox(height: 400.0, child: Text('d')),
SizedBox(height: 400.0, child: Text('e')),
]),
),
],
),
),
);
}
Future<void> testWithConstChildDelegate(WidgetTester tester, double offset) {
return tester.pumpWidget( return tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
...@@ -16,7 +38,7 @@ Future<void> test(WidgetTester tester, double offset) { ...@@ -16,7 +38,7 @@ Future<void> test(WidgetTester tester, double offset) {
offset: ViewportOffset.fixed(offset), offset: ViewportOffset.fixed(offset),
slivers: const <Widget>[ slivers: const <Widget>[
SliverList( SliverList(
delegate: SliverChildListDelegate(<Widget>[ delegate: SliverChildListDelegate.fixed(<Widget>[
SizedBox(height: 400.0, child: Text('a')), SizedBox(height: 400.0, child: Text('a')),
SizedBox(height: 400.0, child: Text('b')), SizedBox(height: 400.0, child: Text('b')),
SizedBox(height: 400.0, child: Text('c')), SizedBox(height: 400.0, child: Text('c')),
...@@ -76,6 +98,39 @@ void main() { ...@@ -76,6 +98,39 @@ void main() {
], 'ab'); ], 'ab');
}); });
testWidgets('Viewport+SliverBlock basic test with constant SliverChildListDelegate', (WidgetTester tester) async {
await testWithConstChildDelegate(tester, 0.0);
expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0)));
verify(tester, <Offset>[
const Offset(0.0, 0.0),
const Offset(0.0, 400.0),
], 'ab');
await testWithConstChildDelegate(tester, 200.0);
verify(tester, <Offset>[
const Offset(0.0, -200.0),
const Offset(0.0, 200.0),
], 'ab');
await testWithConstChildDelegate(tester, 600.0);
verify(tester, <Offset>[
const Offset(0.0, -200.0),
const Offset(0.0, 200.0),
], 'bc');
await testWithConstChildDelegate(tester, 900.0);
verify(tester, <Offset>[
const Offset(0.0, -100.0),
const Offset(0.0, 300.0),
], 'cd');
await testWithConstChildDelegate(tester, 200.0);
verify(tester, <Offset>[
const Offset(0.0, -200.0),
const Offset(0.0, 200.0),
], 'ab');
});
testWidgets('Viewport with GlobalKey reparenting', (WidgetTester tester) async { testWidgets('Viewport with GlobalKey reparenting', (WidgetTester tester) async {
final Key key1 = GlobalKey(); final Key key1 = GlobalKey();
final ViewportOffset offset = ViewportOffset.zero(); final ViewportOffset offset = ViewportOffset.zero();
...@@ -150,9 +205,9 @@ void main() { ...@@ -150,9 +205,9 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Viewport( child: Viewport(
offset: offset, offset: offset,
slivers: const <Widget>[ slivers: <Widget>[
SliverList( SliverList(
delegate: SliverChildListDelegate(<Widget>[ delegate: SliverChildListDelegate(const <Widget>[
SizedBox(height: 251.0, child: Text('a')), SizedBox(height: 251.0, child: Text('a')),
SizedBox(height: 252.0, child: Text('b')), SizedBox(height: 252.0, child: Text('b')),
]), ]),
...@@ -261,9 +316,9 @@ void main() { ...@@ -261,9 +316,9 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Viewport( child: Viewport(
offset: ViewportOffset.zero(), offset: ViewportOffset.zero(),
slivers: const <Widget>[ slivers: <Widget>[
SliverList( SliverList(
delegate: SliverChildListDelegate(<Widget>[ delegate: SliverChildListDelegate(const <Widget>[
SizedBox(height: 400.0, child: Text('a')), SizedBox(height: 400.0, child: Text('a')),
]), ]),
), ),
...@@ -279,9 +334,9 @@ void main() { ...@@ -279,9 +334,9 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Viewport( child: Viewport(
offset: ViewportOffset.fixed(100.0), offset: ViewportOffset.fixed(100.0),
slivers: const <Widget>[ slivers: <Widget>[
SliverList( SliverList(
delegate: SliverChildListDelegate(<Widget>[ delegate: SliverChildListDelegate(const <Widget>[
SizedBox(height: 400.0, child: Text('a')), SizedBox(height: 400.0, child: Text('a')),
]), ]),
), ),
...@@ -297,9 +352,9 @@ void main() { ...@@ -297,9 +352,9 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Viewport( child: Viewport(
offset: ViewportOffset.fixed(100.0), offset: ViewportOffset.fixed(100.0),
slivers: const <Widget>[ slivers: <Widget>[
SliverList( SliverList(
delegate: SliverChildListDelegate(<Widget>[ delegate: SliverChildListDelegate(const <Widget>[
SizedBox(height: 4000.0, child: Text('a')), SizedBox(height: 4000.0, child: Text('a')),
]), ]),
), ),
...@@ -315,9 +370,9 @@ void main() { ...@@ -315,9 +370,9 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Viewport( child: Viewport(
offset: ViewportOffset.zero(), offset: ViewportOffset.zero(),
slivers: const <Widget>[ slivers: <Widget>[
SliverList( SliverList(
delegate: SliverChildListDelegate(<Widget>[ delegate: SliverChildListDelegate(const <Widget>[
SizedBox(height: 4000.0, child: Text('a')), SizedBox(height: 4000.0, child: Text('a')),
]), ]),
), ),
......
// Copyright 2019 The Chromium 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_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
void main() {
testWidgets('Sliver with keep alive without key - should dispose after reodering', (WidgetTester tester) async {
List<Widget> childList= <Widget>[
WidgetTest0(text: 'child 0', keepAlive: true),
WidgetTest1(text: 'child 1', keepAlive: true),
WidgetTest2(text: 'child 2', keepAlive: true),
];
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 2', skipOffstage: false), findsNothing);
childList = createSwitchedChildList(childList, 0, 2);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2));
expect(find.text('child 2'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 0', skipOffstage: false), findsNothing);
expect(state0.hasBeenDisposed, true);
expect(state2.hasBeenDisposed, false);
});
testWidgets('Sliver without keep alive without key - should dispose after reodering', (WidgetTester tester) async {
List<Widget> childList= <Widget>[
WidgetTest0(text: 'child 0'),
WidgetTest1(text: 'child 1'),
WidgetTest2(text: 'child 2'),
];
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 2', skipOffstage: false), findsNothing);
childList = createSwitchedChildList(childList, 0, 2);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2));
expect(find.text('child 2'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 0', skipOffstage: false), findsNothing);
expect(state0.hasBeenDisposed, true);
expect(state2.hasBeenDisposed, false);
});
testWidgets('Sliver without keep alive with key - should dispose after reodering', (WidgetTester tester) async {
List<Widget> childList= <Widget>[
WidgetTest0(text: 'child 0', key: GlobalKey()),
WidgetTest1(text: 'child 1', key: GlobalKey()),
WidgetTest2(text: 'child 2', key: GlobalKey()),
];
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 2', skipOffstage: false), findsNothing);
childList = createSwitchedChildList(childList, 0, 2);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2));
expect(find.text('child 2'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 0', skipOffstage: false), findsNothing);
expect(state0.hasBeenDisposed, true);
expect(state2.hasBeenDisposed, false);
});
testWidgets('Sliver with keep alive with key - should not dispose after reodering', (WidgetTester tester) async {
List<Widget> childList= <Widget>[
WidgetTest0(text: 'child 0', key: GlobalKey(), keepAlive: true),
WidgetTest1(text: 'child 1', key: GlobalKey(), keepAlive: true),
WidgetTest2(text: 'child 2', key: GlobalKey(), keepAlive: true),
];
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 2', skipOffstage: false), findsNothing);
childList = createSwitchedChildList(childList, 0, 2);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2));
expect(find.text('child 2'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 0', skipOffstage: false), findsOneWidget);
expect(state0.hasBeenDisposed, false);
expect(state2.hasBeenDisposed, false);
});
testWidgets('Sliver with keep alive with Unique key - should not dispose after reodering', (WidgetTester tester) async {
List<Widget> childList= <Widget>[
WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true),
WidgetTest1(text: 'child 1', key: UniqueKey(), keepAlive: true),
WidgetTest2(text: 'child 2', key: UniqueKey(), keepAlive: true),
];
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 2', skipOffstage: false), findsNothing);
childList = createSwitchedChildList(childList, 0, 2);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2));
expect(find.text('child 2'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 0', skipOffstage: false), findsOneWidget);
expect(state0.hasBeenDisposed, false);
expect(state2.hasBeenDisposed, false);
});
testWidgets('Sliver with keep alive with Value key - should not dispose after reodering', (WidgetTester tester) async {
List<Widget> childList= <Widget>[
WidgetTest0(text: 'child 0', key: const ValueKey<int>(0), keepAlive: true),
WidgetTest1(text: 'child 1', key: const ValueKey<int>(1), keepAlive: true),
WidgetTest2(text: 'child 2', key: const ValueKey<int>(2), keepAlive: true),
];
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 2', skipOffstage: false), findsNothing);
childList = createSwitchedChildList(childList, 0, 2);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2));
expect(find.text('child 2'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 0', skipOffstage: false), findsOneWidget);
expect(state0.hasBeenDisposed, false);
expect(state2.hasBeenDisposed, false);
});
testWidgets('Sliver complex case 1', (WidgetTester tester) async {
List<Widget> childList= <Widget>[
WidgetTest0(text: 'child 0', key: GlobalKey(), keepAlive: true),
WidgetTest1(text: 'child 1', key: GlobalKey(), keepAlive: true),
WidgetTest2(text: 'child 2', keepAlive: true),
];
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 2', skipOffstage: false), findsNothing);
childList = createSwitchedChildList(childList, 0, 2);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2));
expect(find.text('child 2'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 0', skipOffstage: false), findsOneWidget);
childList = createSwitchedChildList(childList, 0, 1);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest1State state1 = tester.state(find.byType(WidgetTest1));
expect(find.text('child 1'), findsOneWidget);
expect(find.text('child 2', skipOffstage: false), findsNothing);
expect(find.text('child 0', skipOffstage: false), findsOneWidget);
childList = createSwitchedChildList(childList, 1, 2);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
expect(find.text('child 1'), findsOneWidget);
expect(find.text('child 0', skipOffstage: false), findsOneWidget);
expect(find.text('child 2', skipOffstage: false), findsNothing);
childList = createSwitchedChildList(childList, 0, 1);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsOneWidget);
expect(find.text('child 2', skipOffstage: false), findsNothing);
expect(state0.hasBeenDisposed, false);
expect(state1.hasBeenDisposed, false);
// Child 2 does not have a key.
expect(state2.hasBeenDisposed, true);
});
testWidgets('Sliver complex case 2', (WidgetTester tester) async {
List<Widget> childList= <Widget>[
WidgetTest0(text: 'child 0', key: GlobalKey(), keepAlive: true),
WidgetTest1(text: 'child 1', key: UniqueKey()),
WidgetTest2(text: 'child 2', keepAlive: true),
];
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 2', skipOffstage: false), findsNothing);
childList = createSwitchedChildList(childList, 0, 2);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2));
expect(find.text('child 2'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 0', skipOffstage: false), findsOneWidget);
childList = createSwitchedChildList(childList, 0, 1);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
final _WidgetTest1State state1 = tester.state(find.byType(WidgetTest1));
expect(find.text('child 1'), findsOneWidget);
expect(find.text('child 2', skipOffstage: false), findsNothing);
expect(find.text('child 0', skipOffstage: false), findsOneWidget);
childList = createSwitchedChildList(childList, 1, 2);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
expect(find.text('child 1'), findsOneWidget);
expect(find.text('child 0', skipOffstage: false), findsOneWidget);
expect(find.text('child 2', skipOffstage: false), findsNothing);
childList = createSwitchedChildList(childList, 0, 1);
await tester.pumpWidget(SwitchingChildListTest(children: childList));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 2', skipOffstage: false), findsNothing);
expect(state0.hasBeenDisposed, false);
expect(state1.hasBeenDisposed, true);
expect(state2.hasBeenDisposed, true);
});
testWidgets('Sliver with SliverChildBuilderDelegate', (WidgetTester tester) async {
List<Widget> childList= <Widget>[
WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true),
WidgetTest1(text: 'child 1', key: GlobalKey()),
WidgetTest2(text: 'child 2', keepAlive: true),
];
await tester.pumpWidget(SwitchingChildBuilderTest(children: childList));
final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 2', skipOffstage: false), findsNothing);
childList = createSwitchedChildList(childList, 0, 2);
await tester.pumpWidget(SwitchingChildBuilderTest(children: childList));
final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2));
expect(find.text('child 2'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 0', skipOffstage: false), findsOneWidget);
childList = createSwitchedChildList(childList, 0, 1);
await tester.pumpWidget(SwitchingChildBuilderTest(children: childList));
final _WidgetTest1State state1 = tester.state(find.byType(WidgetTest1));
expect(find.text('child 1'), findsOneWidget);
expect(find.text('child 2', skipOffstage: false), findsNothing);
expect(find.text('child 0', skipOffstage: false), findsOneWidget);
childList = createSwitchedChildList(childList, 1, 2);
await tester.pumpWidget(SwitchingChildBuilderTest(children: childList));
expect(find.text('child 1'), findsOneWidget);
expect(find.text('child 0', skipOffstage: false), findsOneWidget);
expect(find.text('child 2', skipOffstage: false), findsNothing);
childList = createSwitchedChildList(childList, 0, 1);
await tester.pumpWidget(SwitchingChildBuilderTest(children: childList));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1', skipOffstage: false), findsNothing);
expect(find.text('child 2', skipOffstage: false), findsNothing);
expect(state0.hasBeenDisposed, false);
expect(state1.hasBeenDisposed, true);
expect(state2.hasBeenDisposed, true);
});
testWidgets('SliverFillViewport should not dispose widget with key during in screen reordering', (WidgetTester tester) async {
List<Widget> childList= <Widget>[
WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true),
WidgetTest1(text: 'child 1', key: UniqueKey()),
WidgetTest2(text: 'child 2', keepAlive: true),
];
await tester.pumpWidget(
SwitchingChildListTest(children: childList, viewportFraction: 0.1)
);
final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0));
final _WidgetTest1State state1 = tester.state(find.byType(WidgetTest1));
final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1'), findsOneWidget);
expect(find.text('child 2'), findsOneWidget);
childList = createSwitchedChildList(childList, 0, 2);
await tester.pumpWidget(
SwitchingChildListTest(children: childList, viewportFraction: 0.1)
);
childList = createSwitchedChildList(childList, 0, 1);
await tester.pumpWidget(
SwitchingChildListTest(children: childList, viewportFraction: 0.1)
);
childList = createSwitchedChildList(childList, 1, 2);
await tester.pumpWidget(
SwitchingChildListTest(children: childList, viewportFraction: 0.1)
);
childList = createSwitchedChildList(childList, 0, 1);
await tester.pumpWidget(
SwitchingChildListTest(children: childList, viewportFraction: 0.1)
);
expect(state0.hasBeenDisposed, false);
expect(state1.hasBeenDisposed, false);
expect(state2.hasBeenDisposed, true);
});
testWidgets('SliverList should not dispose widget with key during in screen reordering', (WidgetTester tester) async {
List<Widget> childList= <Widget>[
WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true),
WidgetTest1(text: 'child 1', keepAlive: true),
WidgetTest2(text: 'child 2', key: UniqueKey()),
];
await tester.pumpWidget(
SwitchingSliverListTest(children: childList, viewportFraction: 0.1)
);
final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0));
final _WidgetTest1State state1 = tester.state(find.byType(WidgetTest1));
final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1'), findsOneWidget);
expect(find.text('child 2'), findsOneWidget);
childList = createSwitchedChildList(childList, 0, 2);
await tester.pumpWidget(
SwitchingSliverListTest(children: childList, viewportFraction: 0.1)
);
childList = createSwitchedChildList(childList, 1, 2);
await tester.pumpWidget(
SwitchingSliverListTest(children: childList, viewportFraction: 0.1)
);
childList = createSwitchedChildList(childList, 1, 2);
await tester.pumpWidget(
SwitchingSliverListTest(children: childList, viewportFraction: 0.1)
);
childList = createSwitchedChildList(childList, 0, 1);
await tester.pumpWidget(
SwitchingSliverListTest(children: childList, viewportFraction: 0.1)
);
childList = createSwitchedChildList(childList, 0, 2);
await tester.pumpWidget(
SwitchingSliverListTest(children: childList, viewportFraction: 0.1)
);
childList = createSwitchedChildList(childList, 0, 1);
await tester.pumpWidget(
SwitchingSliverListTest(children: childList, viewportFraction: 0.1)
);
expect(state0.hasBeenDisposed, false);
expect(state1.hasBeenDisposed, true);
expect(state2.hasBeenDisposed, false);
});
testWidgets('SliverList remove child from child list', (WidgetTester tester) async {
List<Widget> childList= <Widget>[
WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true),
WidgetTest1(text: 'child 1', keepAlive: true),
WidgetTest2(text: 'child 2', key: UniqueKey()),
];
await tester.pumpWidget(
SwitchingSliverListTest(children: childList, viewportFraction: 0.1)
);
final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0));
final _WidgetTest1State state1 = tester.state(find.byType(WidgetTest1));
final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2));
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1'), findsOneWidget);
expect(find.text('child 2'), findsOneWidget);
childList = createSwitchedChildList(childList, 0, 1);
childList.removeAt(2);
await tester.pumpWidget(
SwitchingSliverListTest(children: childList, viewportFraction: 0.1)
);
expect(find.text('child 0'), findsOneWidget);
expect(find.text('child 1'), findsOneWidget);
expect(find.text('child 2'), findsNothing);
expect(state0.hasBeenDisposed, false);
expect(state1.hasBeenDisposed, true);
expect(state2.hasBeenDisposed, true);
});
}
List<Widget> createSwitchedChildList(List<Widget> childList, int i, int j) {
final Widget w = childList[i];
childList[i] = childList[j];
childList[j] = w;
return List<Widget>.from(childList);
}
class SwitchingChildBuilderTest extends StatefulWidget {
SwitchingChildBuilderTest({
this.children,
Key key
}) : super(key: key);
final List<Widget> children;
@override
_SwitchingChildBuilderTest createState() => _SwitchingChildBuilderTest();
}
class _SwitchingChildBuilderTest extends State<SwitchingChildBuilderTest> {
List<Widget> children;
Map<Key, int> _mapKeyToIndex;
@override
void initState() {
super.initState();
children = widget.children;
_mapKeyToIndex = <Key, int>{};
for (int index = 0; index < children.length; index += 1) {
final Key key = children[index].key;
if (key != null) {
_mapKeyToIndex[key] = index;
}
}
}
@override
void didUpdateWidget(SwitchingChildBuilderTest oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.children != widget.children) {
children = widget.children;
_mapKeyToIndex = <Key, int>{};
for (int index = 0; index < children.length; index += 1) {
final Key key = children[index].key;
if (key != null) {
_mapKeyToIndex[key] = index;
}
}
}
}
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Container(
height: 100,
child: CustomScrollView(
cacheExtent: 0,
slivers: <Widget>[
SliverFillViewport(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return children[index];
},
childCount: children.length,
findChildIndexCallback: (Key key) {
return _mapKeyToIndex[key] == null ? -1 : _mapKeyToIndex[key];
}
),
)
],
),
),
),
);
}
}
class SwitchingChildListTest extends StatefulWidget {
SwitchingChildListTest({
this.children,
this.viewportFraction = 1.0,
Key key
}) : super(key: key);
final List<Widget> children;
final double viewportFraction;
@override
_SwitchingChildListTest createState() => _SwitchingChildListTest();
}
class _SwitchingChildListTest extends State<SwitchingChildListTest> {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Container(
height: 100,
child: CustomScrollView(
cacheExtent: 0,
slivers: <Widget>[
SliverFillViewport(
viewportFraction: widget.viewportFraction,
delegate: SliverChildListDelegate(widget.children),
)
],
),
),
),
);
}
}
class SwitchingSliverListTest extends StatefulWidget {
SwitchingSliverListTest({
this.children,
this.viewportFraction = 1.0,
Key key
}) : super(key: key);
final List<Widget> children;
final double viewportFraction;
@override
_SwitchingSliverListTest createState() => _SwitchingSliverListTest();
}
class _SwitchingSliverListTest extends State<SwitchingSliverListTest> {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Container(
height: 100,
child: CustomScrollView(
cacheExtent: 0,
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate(widget.children),
)
],
),
),
),
);
}
}
class WidgetTest0 extends StatefulWidget {
WidgetTest0({
this.text,
this.keepAlive = false,
Key key
}) : super(key: key);
final String text;
final bool keepAlive;
@override
_WidgetTest0State createState() => _WidgetTest0State();
}
class _WidgetTest0State extends State<WidgetTest0> with AutomaticKeepAliveClientMixin{
bool hasBeenDisposed = false;
@override
Widget build(BuildContext context) {
super.build(context);
return Text(widget.text);
}
@override
void dispose() {
hasBeenDisposed = true;
super.dispose();
}
@override
bool get wantKeepAlive => widget.keepAlive;
}
class WidgetTest1 extends StatefulWidget {
WidgetTest1({
this.text,
this.keepAlive = false,
Key key
}) : super(key: key);
final String text;
final bool keepAlive;
@override
_WidgetTest1State createState() => _WidgetTest1State();
}
class _WidgetTest1State extends State<WidgetTest1> with AutomaticKeepAliveClientMixin{
bool hasBeenDisposed = false;
@override
Widget build(BuildContext context) {
super.build(context);
return Text(widget.text);
}
@override
void dispose() {
hasBeenDisposed = true;
super.dispose();
}
@override
bool get wantKeepAlive => widget.keepAlive;
}
class WidgetTest2 extends StatefulWidget {
WidgetTest2({
this.text,
this.keepAlive = false,
Key key
}) : super(key: key);
final String text;
final bool keepAlive;
@override
_WidgetTest2State createState() => _WidgetTest2State();
}
class _WidgetTest2State extends State<WidgetTest2> with AutomaticKeepAliveClientMixin{
bool hasBeenDisposed = false;
@override
Widget build(BuildContext context) {
super.build(context);
return Text(widget.text);
}
@override
void dispose() {
hasBeenDisposed = true;
super.dispose();
}
@override
bool get wantKeepAlive => widget.keepAlive;
}
...@@ -206,7 +206,8 @@ void main() { ...@@ -206,7 +206,8 @@ void main() {
addRepaintBoundaries: false, addRepaintBoundaries: false,
addSemanticIndexes: false, addSemanticIndexes: false,
); );
expect(builderThrowsDelegate.build(null, 0), errorText); final KeyedSubtree wrapped = builderThrowsDelegate.build(null, 0);
expect(wrapped.child, errorText);
expect(tester.takeException(), 'builder'); expect(tester.takeException(), 'builder');
ErrorWidget.builder = oldBuilder; ErrorWidget.builder = oldBuilder;
}); });
......
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