Fix a [_ViewportElement] RenderObjectChild update bug (#96377)
diff --git a/packages/flutter/lib/src/widgets/viewport.dart b/packages/flutter/lib/src/widgets/viewport.dart
index 17d4607..dc50654 100644
--- a/packages/flutter/lib/src/widgets/viewport.dart
+++ b/packages/flutter/lib/src/widgets/viewport.dart
@@ -211,6 +211,9 @@
/// Creates an element that uses the given widget as its configuration.
_ViewportElement(Viewport widget) : super(widget);
+ bool _doingMountOrUpdate = false;
+ int? _centerSlotIndex;
+
@override
Viewport get widget => super.widget as Viewport;
@@ -219,26 +222,67 @@
@override
void mount(Element? parent, Object? newSlot) {
+ assert(!_doingMountOrUpdate);
+ _doingMountOrUpdate = true;
super.mount(parent, newSlot);
_updateCenter();
+ assert(_doingMountOrUpdate);
+ _doingMountOrUpdate = false;
}
@override
void update(MultiChildRenderObjectWidget newWidget) {
+ assert(!_doingMountOrUpdate);
+ _doingMountOrUpdate = true;
super.update(newWidget);
_updateCenter();
+ assert(_doingMountOrUpdate);
+ _doingMountOrUpdate = false;
}
void _updateCenter() {
// TODO(ianh): cache the keys to make this faster
if (widget.center != null) {
- renderObject.center = children.singleWhere(
- (Element element) => element.widget.key == widget.center,
- ).renderObject as RenderSliver?;
+ int elementIndex = 0;
+ for (final Element e in children) {
+ if (e.widget.key == widget.center) {
+ renderObject.center = e.renderObject as RenderSliver?;
+ break;
+ }
+ elementIndex++;
+ }
+ assert(elementIndex < children.length);
+ _centerSlotIndex = elementIndex;
} else if (children.isNotEmpty) {
renderObject.center = children.first.renderObject as RenderSliver?;
+ _centerSlotIndex = 0;
} else {
renderObject.center = null;
+ _centerSlotIndex = null;
+ }
+ }
+
+ @override
+ void insertRenderObjectChild(RenderObject child, IndexedSlot<Element?> slot) {
+ super.insertRenderObjectChild(child, slot);
+ // Once [mount]/[update] are done, the `renderObject.center` will be updated
+ // in [_updateCenter].
+ if (!_doingMountOrUpdate && slot.index == _centerSlotIndex) {
+ renderObject.center = child as RenderSliver?;
+ }
+ }
+
+ @override
+ void moveRenderObjectChild(RenderObject child, IndexedSlot<Element?> oldSlot, IndexedSlot<Element?> newSlot) {
+ super.moveRenderObjectChild(child, oldSlot, newSlot);
+ assert(_doingMountOrUpdate);
+ }
+
+ @override
+ void removeRenderObjectChild(RenderObject child, Object? slot) {
+ super.removeRenderObjectChild(child, slot);
+ if (!_doingMountOrUpdate && renderObject.center == child) {
+ renderObject.center = null;
}
}
diff --git a/packages/flutter/test/widgets/custom_scroll_view_test.dart b/packages/flutter/test/widgets/custom_scroll_view_test.dart
index 9e1aa36..02c3281 100644
--- a/packages/flutter/test/widgets/custom_scroll_view_test.dart
+++ b/packages/flutter/test/widgets/custom_scroll_view_test.dart
@@ -6,6 +6,81 @@
import 'package:flutter_test/flutter_test.dart';
void main() {
+ // Regression test for https://github.com/flutter/flutter/issues/96024
+ testWidgets('CustomScrollView.center update test 1', (WidgetTester tester) async {
+ final Key centerKey = UniqueKey();
+ late StateSetter setState;
+ bool hasKey = false;
+ await tester.pumpWidget(Directionality(
+ textDirection: TextDirection.ltr,
+ child: CustomScrollView(
+ center: centerKey,
+ slivers: <Widget>[
+ const SliverToBoxAdapter(key: Key('a'), child: SizedBox(height: 100.0)),
+ StatefulBuilder(
+ key: centerKey,
+ builder: (BuildContext context, StateSetter setter) {
+ setState = setter;
+ if (hasKey) {
+ return const SliverToBoxAdapter(
+ key: Key('b'),
+ child: SizedBox(height: 100.0),
+ );
+ } else {
+ return const SliverToBoxAdapter(
+ child: SizedBox(height: 100.0),
+ );
+ }
+ },
+ ),
+ ],
+ ),
+ ));
+ await tester.pumpAndSettle();
+
+ // Change the center key will trigger the old RenderObject remove and a new
+ // RenderObject insert.
+ setState(() {
+ hasKey = true;
+ });
+
+ await tester.pumpAndSettle();
+
+ // Pass without throw.
+ });
+
+ testWidgets('CustomScrollView.center update test 2', (WidgetTester tester) async {
+ const List<Widget> slivers1 = <Widget>[
+ SliverToBoxAdapter(key: Key('a'), child: SizedBox(height: 100.0)),
+ SliverToBoxAdapter(key: Key('b'), child: SizedBox(height: 100.0)),
+ SliverToBoxAdapter(key: Key('c'), child: SizedBox(height: 100.0)),
+ ];
+
+ const List<Widget> slivers2 = <Widget>[
+ SliverToBoxAdapter(key: Key('c'), child: SizedBox(height: 100.0)),
+ SliverToBoxAdapter(key: Key('d'), child: SizedBox(height: 100.0)),
+ SliverToBoxAdapter(key: Key('a'), child: SizedBox(height: 100.0)),
+ ];
+
+ Widget buildFrame(List<Widget> slivers, Key center) {
+ return Directionality(
+ textDirection: TextDirection.ltr,
+ child: CustomScrollView(
+ center: center,
+ slivers: slivers,
+ ),
+ );
+ }
+
+ await tester.pumpWidget(buildFrame(slivers1, const Key('b')));
+ await tester.pumpAndSettle();
+
+ await tester.pumpWidget(buildFrame(slivers2, const Key('d')));
+ await tester.pumpAndSettle();
+
+ // Pass without throw.
+ });
+
testWidgets('CustomScrollView.center', (WidgetTester tester) async {
await tester.pumpWidget(const Directionality(
textDirection: TextDirection.ltr,