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;
|
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> {
|
|||||||
@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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
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 {
|
|||||||
/// 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
|
|||||||
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
|
|||||||
/// 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
|
|||||||
|
|
||||||
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) {
|
|
||||||
assert(indexOf(child) > index);
|
|
||||||
index = indexOf(child);
|
|
||||||
child = childAfter(child);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
assert(() {
|
||||||
|
_debugDanglingKeepAlives.remove(child);
|
||||||
return true;
|
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
|
@override
|
||||||
@ -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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
|||||||
/// 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 {
|
|||||||
/// 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 {
|
|||||||
}
|
}
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
}
|
}
|
||||||
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
|
|||||||
_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
|
|||||||
_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
|
|||||||
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
|
|||||||
|
|
||||||
@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> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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() {
|
|||||||
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';
|
|||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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';
|
|||||||
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) {
|
|||||||
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() {
|
|||||||
], '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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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')),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
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,
|
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;
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user