Reland fix 25807 implement move for sliver multibox widget (#31978)
This commit is contained in:
parent
39d660be78
commit
38808d9fe4
@ -1125,6 +1125,7 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
TabController _controller;
|
||||
PageController _pageController;
|
||||
List<Widget> _children;
|
||||
List<Widget> _childrenWithKey;
|
||||
int _currentIndex;
|
||||
int _warpUnderwayCount = 0;
|
||||
|
||||
@ -1156,7 +1157,7 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_children = widget.children;
|
||||
_updateChildren();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1173,7 +1174,7 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
if (widget.controller != oldWidget.controller)
|
||||
_updateTabController();
|
||||
if (widget.children != oldWidget.children && _warpUnderwayCount == 0)
|
||||
_children = widget.children;
|
||||
_updateChildren();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1184,6 +1185,11 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateChildren() {
|
||||
_children = widget.children;
|
||||
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
|
||||
}
|
||||
|
||||
void _handleTabControllerAnimationTick() {
|
||||
if (_warpUnderwayCount > 0 || !_controller.indexIsChanging)
|
||||
return; // This widget is driving the controller's animation.
|
||||
@ -1206,28 +1212,30 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
return _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
|
||||
|
||||
assert((_currentIndex - previousIndex).abs() > 1);
|
||||
int initialPage;
|
||||
final int initialPage = _currentIndex > previousIndex
|
||||
? _currentIndex - 1
|
||||
: _currentIndex + 1;
|
||||
final List<Widget> originalChildren = _childrenWithKey;
|
||||
setState(() {
|
||||
_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);
|
||||
|
||||
await _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease);
|
||||
if (!mounted)
|
||||
return Future<void>.value();
|
||||
|
||||
setState(() {
|
||||
_warpUnderwayCount -= 1;
|
||||
_children = widget.children;
|
||||
if (widget.children != _children) {
|
||||
_updateChildren();
|
||||
} else {
|
||||
_childrenWithKey = originalChildren;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1272,7 +1280,7 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
controller: _pageController,
|
||||
physics: widget.physics == null ? _kTabBarViewPhysics : _kTabBarViewPhysics.applyTo(widget.physics),
|
||||
children: _children,
|
||||
children: _childrenWithKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -85,7 +85,8 @@ abstract class RenderSliverBoxChildManager {
|
||||
/// list).
|
||||
int get childCount;
|
||||
|
||||
/// Called during [RenderSliverMultiBoxAdaptor.adoptChild].
|
||||
/// Called during [RenderSliverMultiBoxAdaptor.adoptChild] or
|
||||
/// [RenderSliverMultiBoxAdaptor.move].
|
||||
///
|
||||
/// Subclasses must ensure that the [SliverMultiBoxAdaptorParentData.index]
|
||||
/// field of the child's [RenderObject.parentData] accurately reflects the
|
||||
@ -193,7 +194,12 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
|
||||
RenderSliverMultiBoxAdaptor({
|
||||
@required RenderSliverBoxChildManager childManager,
|
||||
}) : assert(childManager != null),
|
||||
_childManager = childManager;
|
||||
_childManager = childManager {
|
||||
assert(() {
|
||||
_debugDanglingKeepAlives = <RenderBox>[];
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderObject child) {
|
||||
@ -214,6 +220,27 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
|
||||
/// The nodes being kept alive despite not being visible.
|
||||
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
|
||||
void adoptChild(RenderObject child) {
|
||||
super.adoptChild(child);
|
||||
@ -224,21 +251,70 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
|
||||
|
||||
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
|
||||
void insert(RenderBox child, { RenderBox after }) {
|
||||
assert(!_keepAliveBucket.containsValue(child));
|
||||
super.insert(child, after: after);
|
||||
assert(firstChild != null);
|
||||
assert(() {
|
||||
int index = indexOf(firstChild);
|
||||
RenderBox child = childAfter(firstChild);
|
||||
while (child != null) {
|
||||
assert(indexOf(child) > index);
|
||||
index = indexOf(child);
|
||||
child = childAfter(child);
|
||||
assert(_debugVerifyChildOrder());
|
||||
}
|
||||
|
||||
@override
|
||||
void move(RenderBox child, { RenderBox after }) {
|
||||
// There are two scenarios:
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
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;
|
||||
}());
|
||||
_keepAliveBucket[childParentData.index] = child;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -249,6 +325,10 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
|
||||
return;
|
||||
}
|
||||
assert(_keepAliveBucket[childParentData.index] == child);
|
||||
assert(() {
|
||||
_debugDanglingKeepAlives.remove(child);
|
||||
return true;
|
||||
}());
|
||||
_keepAliveBucket.remove(childParentData.index);
|
||||
dropChild(child);
|
||||
}
|
||||
|
@ -149,6 +149,13 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
|
||||
assert(() {
|
||||
assert(parent != null);
|
||||
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
|
||||
// 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
|
||||
|
@ -174,6 +174,13 @@ abstract class SliverChildDelegate {
|
||||
/// away.
|
||||
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
|
||||
String toString() {
|
||||
final List<String> description = <String>[];
|
||||
@ -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.
|
||||
///
|
||||
/// Many slivers lazily construct their box children to avoid creating more
|
||||
@ -306,8 +319,14 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
|
||||
/// The [builder], [addAutomaticKeepAlives], [addRepaintBoundaries],
|
||||
/// [addSemanticIndexes], and [semanticIndexCallback] arguments must not be
|
||||
/// 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(
|
||||
this.builder, {
|
||||
this.findChildIndexCallback,
|
||||
this.childCount,
|
||||
this.addAutomaticKeepAlives = true,
|
||||
this.addRepaintBoundaries = true,
|
||||
@ -388,6 +407,31 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
|
||||
/// Defaults to providing an index for each widget.
|
||||
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
|
||||
Widget build(BuildContext context, int index) {
|
||||
assert(builder != null);
|
||||
@ -401,8 +445,9 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
|
||||
}
|
||||
if (child == null)
|
||||
return null;
|
||||
final Key key = child.key != null ? _SaltedValueKey(child.key) : null;
|
||||
if (addRepaintBoundaries)
|
||||
child = RepaintBoundary.wrap(child, index);
|
||||
child = RepaintBoundary(child: child);
|
||||
if (addSemanticIndexes) {
|
||||
final int semanticIndex = semanticIndexCallback(child, index);
|
||||
if (semanticIndex != null)
|
||||
@ -410,7 +455,7 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
|
||||
}
|
||||
if (addAutomaticKeepAlives)
|
||||
child = AutomaticKeepAlive(child: child);
|
||||
return child;
|
||||
return KeyedSubtree(child: child, key: key);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -478,7 +523,10 @@ class SliverChildListDelegate extends SliverChildDelegate {
|
||||
/// The [children], [addAutomaticKeepAlives], [addRepaintBoundaries],
|
||||
/// [addSemanticIndexes], and [semanticIndexCallback] arguments must not be
|
||||
/// null.
|
||||
const SliverChildListDelegate(
|
||||
///
|
||||
/// If the order of children` never changes, consider using the constant
|
||||
/// [SliverChildListDelegate.fixed] constructor.
|
||||
SliverChildListDelegate(
|
||||
this.children, {
|
||||
this.addAutomaticKeepAlives = true,
|
||||
this.addRepaintBoundaries = true,
|
||||
@ -489,7 +537,31 @@ class SliverChildListDelegate extends SliverChildDelegate {
|
||||
assert(addAutomaticKeepAlives != null),
|
||||
assert(addRepaintBoundaries != 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].
|
||||
///
|
||||
@ -544,15 +616,63 @@ class SliverChildListDelegate extends SliverChildDelegate {
|
||||
/// The widgets to display.
|
||||
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
|
||||
Widget build(BuildContext context, int index) {
|
||||
assert(children != null);
|
||||
if (index < 0 || index >= children.length)
|
||||
return null;
|
||||
Widget child = children[index];
|
||||
final Key key = child.key != null? _SaltedValueKey(child.key) : null;
|
||||
assert(child != null);
|
||||
if (addRepaintBoundaries)
|
||||
child = RepaintBoundary.wrap(child, index);
|
||||
child = RepaintBoundary(child: child);
|
||||
if (addSemanticIndexes) {
|
||||
final int semanticIndex = semanticIndexCallback(child, index);
|
||||
if (semanticIndex != null)
|
||||
@ -560,7 +680,7 @@ class SliverChildListDelegate extends SliverChildDelegate {
|
||||
}
|
||||
if (addAutomaticKeepAlives)
|
||||
child = AutomaticKeepAlive(child: child);
|
||||
return child;
|
||||
return KeyedSubtree(child: child, key: key);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -979,9 +1099,15 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
|
||||
_currentBeforeChild = null;
|
||||
assert(_currentlyUpdatingChildIndex == null);
|
||||
try {
|
||||
final SplayTreeMap<int, Element> newChildren = SplayTreeMap<int, Element>();
|
||||
|
||||
void processElement(int 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) {
|
||||
_childElements[index] = newChild;
|
||||
final SliverMultiBoxAdaptorParentData parentData = newChild.renderObject.parentData;
|
||||
@ -991,14 +1117,32 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
|
||||
_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) {
|
||||
final int lastKey = _childElements.lastKey() ?? -1;
|
||||
processElement(lastKey + 1);
|
||||
final int rightBoundary = lastKey + 1;
|
||||
newChildren[rightBoundary] = _childElements[rightBoundary];
|
||||
processElement(rightBoundary);
|
||||
}
|
||||
} finally {
|
||||
_currentlyUpdatingChildIndex = null;
|
||||
renderObject.debugChildIntegrityEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1038,7 +1182,6 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
|
||||
if (oldParentData != newParentData && oldParentData != null && newParentData != null) {
|
||||
newParentData.layoutOffset = oldParentData.layoutOffset;
|
||||
}
|
||||
|
||||
return newChild;
|
||||
}
|
||||
|
||||
@ -1163,9 +1306,9 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
|
||||
|
||||
@override
|
||||
void moveChildRenderObject(covariant RenderObject child, int slot) {
|
||||
// TODO(ianh): At some point we should be better about noticing when a
|
||||
// particular LocalKey changes slot, and handle moving the nodes around.
|
||||
assert(false);
|
||||
assert(slot != null);
|
||||
assert(_currentlyUpdatingChildIndex == slot);
|
||||
renderObject.move(child, after: _currentBeforeChild);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -49,6 +49,7 @@ class StateMarkerState extends State<StateMarker> {
|
||||
}
|
||||
|
||||
class AlwaysKeepAliveWidget extends StatefulWidget {
|
||||
const AlwaysKeepAliveWidget({ Key key}) : super(key: key);
|
||||
static String text = 'AlwaysKeepAlive';
|
||||
@override
|
||||
AlwaysKeepAliveState createState() => AlwaysKeepAliveState();
|
||||
@ -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 {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/11895
|
||||
final List<String> tabs = <String>[
|
||||
@ -2018,7 +2069,7 @@ void main() {
|
||||
body: TabBarView(
|
||||
controller: controller,
|
||||
children: <Widget>[
|
||||
AlwaysKeepAliveWidget(),
|
||||
AlwaysKeepAliveWidget(key: UniqueKey()),
|
||||
const Text('2'),
|
||||
const Text('3'),
|
||||
const Text('4'),
|
||||
|
@ -9,10 +9,10 @@ import 'package:flutter/widgets.dart';
|
||||
void main() {
|
||||
testWidgets('Sliver in a box', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const DecoratedBox(
|
||||
decoration: BoxDecoration(),
|
||||
DecoratedBox(
|
||||
decoration: const BoxDecoration(),
|
||||
child: SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[]),
|
||||
delegate: SliverChildListDelegate(const <Widget>[]),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -21,9 +21,9 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
Row(
|
||||
children: const <Widget>[
|
||||
children: <Widget>[
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[]),
|
||||
delegate: SliverChildListDelegate(const <Widget>[]),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -451,6 +451,41 @@ void main() {
|
||||
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 {
|
||||
StateSetter setState;
|
||||
|
||||
|
@ -22,12 +22,12 @@ void main() {
|
||||
data: const MediaQueryData(),
|
||||
child: CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: const <Widget>[
|
||||
SliverAppBar(floating: true, pinned: true, expandedHeight: 200.0, title: Text('A')),
|
||||
SliverAppBar(primary: false, pinned: true, title: Text('B')),
|
||||
slivers: <Widget>[
|
||||
const SliverAppBar(floating: true, pinned: true, expandedHeight: 200.0, title: Text('A')),
|
||||
const SliverAppBar(primary: false, pinned: true, title: Text('B')),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(
|
||||
<Widget>[
|
||||
const <Widget>[
|
||||
Text('C'),
|
||||
Text('D'),
|
||||
SizedBox(height: 500.0),
|
||||
|
@ -203,9 +203,9 @@ void main() {
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: <Widget>[
|
||||
SliverPersistentHeader(delegate: TestDelegate(), floating: true),
|
||||
const SliverList(
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 300.0,
|
||||
child: Text('X'),
|
||||
),
|
||||
|
@ -258,9 +258,9 @@ void main() {
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: <Widget>[
|
||||
SliverPersistentHeader(delegate: TestDelegate(), pinned: true),
|
||||
const SliverList(
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 300.0,
|
||||
child: Text('X'),
|
||||
),
|
||||
|
@ -82,8 +82,40 @@ void main() {
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: <Widget>[
|
||||
SliverPersistentHeader(delegate: TestDelegate()),
|
||||
const SliverList(
|
||||
SliverList(
|
||||
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(
|
||||
height: 300.0,
|
||||
child: Text('X'),
|
||||
|
@ -9,6 +9,28 @@ import 'package:flutter/widgets.dart';
|
||||
import '../rendering/mock_canvas.dart';
|
||||
|
||||
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(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
@ -16,7 +38,7 @@ Future<void> test(WidgetTester tester, double offset) {
|
||||
offset: ViewportOffset.fixed(offset),
|
||||
slivers: const <Widget>[
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[
|
||||
delegate: SliverChildListDelegate.fixed(<Widget>[
|
||||
SizedBox(height: 400.0, child: Text('a')),
|
||||
SizedBox(height: 400.0, child: Text('b')),
|
||||
SizedBox(height: 400.0, child: Text('c')),
|
||||
@ -76,6 +98,39 @@ void main() {
|
||||
], '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 {
|
||||
final Key key1 = GlobalKey();
|
||||
final ViewportOffset offset = ViewportOffset.zero();
|
||||
@ -150,9 +205,9 @@ void main() {
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Viewport(
|
||||
offset: offset,
|
||||
slivers: const <Widget>[
|
||||
slivers: <Widget>[
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[
|
||||
delegate: SliverChildListDelegate(const <Widget>[
|
||||
SizedBox(height: 251.0, child: Text('a')),
|
||||
SizedBox(height: 252.0, child: Text('b')),
|
||||
]),
|
||||
@ -261,9 +316,9 @@ void main() {
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Viewport(
|
||||
offset: ViewportOffset.zero(),
|
||||
slivers: const <Widget>[
|
||||
slivers: <Widget>[
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[
|
||||
delegate: SliverChildListDelegate(const <Widget>[
|
||||
SizedBox(height: 400.0, child: Text('a')),
|
||||
]),
|
||||
),
|
||||
@ -279,9 +334,9 @@ void main() {
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Viewport(
|
||||
offset: ViewportOffset.fixed(100.0),
|
||||
slivers: const <Widget>[
|
||||
slivers: <Widget>[
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[
|
||||
delegate: SliverChildListDelegate(const <Widget>[
|
||||
SizedBox(height: 400.0, child: Text('a')),
|
||||
]),
|
||||
),
|
||||
@ -297,9 +352,9 @@ void main() {
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Viewport(
|
||||
offset: ViewportOffset.fixed(100.0),
|
||||
slivers: const <Widget>[
|
||||
slivers: <Widget>[
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[
|
||||
delegate: SliverChildListDelegate(const <Widget>[
|
||||
SizedBox(height: 4000.0, child: Text('a')),
|
||||
]),
|
||||
),
|
||||
@ -315,9 +370,9 @@ void main() {
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Viewport(
|
||||
offset: ViewportOffset.zero(),
|
||||
slivers: const <Widget>[
|
||||
slivers: <Widget>[
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[
|
||||
delegate: SliverChildListDelegate(const <Widget>[
|
||||
SizedBox(height: 4000.0, child: Text('a')),
|
||||
]),
|
||||
),
|
||||
|
646
packages/flutter/test/widgets/slivers_keepalive_test.dart
Normal file
646
packages/flutter/test/widgets/slivers_keepalive_test.dart
Normal file
@ -0,0 +1,646 @@
|
||||
// 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() {
|
||||
addRepaintBoundaries: false,
|
||||
addSemanticIndexes: false,
|
||||
);
|
||||
expect(builderThrowsDelegate.build(null, 0), errorText);
|
||||
final KeyedSubtree wrapped = builderThrowsDelegate.build(null, 0);
|
||||
expect(wrapped.child, errorText);
|
||||
expect(tester.takeException(), 'builder');
|
||||
ErrorWidget.builder = oldBuilder;
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user