Fix update order of SliverAppBar (#158159)

Fixes #158158

Similar to 154484, but addressing more fundamental causes. We should call the child's `didUpdateWidget` before the child's `build` when states change.

1. Remove redundant code but keep the old test because we don't need it after this fix.
2. Add getters of `lastShrinkOffset` and `lastOverlapsContent` that are needed when `SliverAppBar.update`.
3. Add a test with a custom TabBar to avoid breaking when some changes to the default TabBar.
This commit is contained in:
hgraceb 2024-11-14 08:08:22 +08:00 committed by GitHub
parent 1a31e396db
commit 5f0f18d739
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 68 additions and 16 deletions

View File

@ -1772,11 +1772,9 @@ class _TabBarState extends State<TabBar> {
wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation, _defaults);
} else {
// The user is dragging the TabBarView's PageView left or right.
if (_currentIndex! < widget.tabs.length) {
final int tabIndex = _currentIndex!;
final Animation<double> centerAnimation = _DragAnimation(_controller!, tabIndex);
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation, _defaults);
}
final int tabIndex = _currentIndex!;
final Animation<double> centerAnimation = _DragAnimation(_controller!, tabIndex);
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation, _defaults);
if (_currentIndex! > 0) {
final int tabIndex = _currentIndex! - 1;
final Animation<double> previousAnimation = ReverseAnimation(_DragAnimation(_controller!, tabIndex));

View File

@ -162,7 +162,13 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
}
bool _needsUpdateChild = true;
/// The most recent `shrinkOffset` passed to [updateChild].
double get lastShrinkOffset => _lastShrinkOffset;
double _lastShrinkOffset = 0.0;
/// The most recent `overlapsContent` passed to [updateChild].
bool get lastOverlapsContent => _lastOverlapsContent;
bool _lastOverlapsContent = false;
/// Defines the parameters used to execute an [AsyncCallback] when a

View File

@ -286,6 +286,8 @@ class _SliverPersistentHeaderElement extends RenderObjectElement {
final SliverPersistentHeaderDelegate oldDelegate = oldWidget.delegate;
if (newDelegate != oldDelegate &&
(newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate))) {
final _RenderSliverPersistentHeaderForWidgetsMixin renderObject = this.renderObject;
_updateChild(newDelegate, renderObject.lastShrinkOffset, renderObject.lastOverlapsContent);
renderObject.triggerRebuild();
}
}
@ -298,20 +300,19 @@ class _SliverPersistentHeaderElement extends RenderObjectElement {
Element? child;
void _updateChild(SliverPersistentHeaderDelegate delegate, double shrinkOffset, bool overlapsContent) {
final Widget newWidget = delegate.build(this, shrinkOffset, overlapsContent);
child = updateChild(
child,
floating ? _FloatingHeader(child: newWidget) : newWidget,
null,
);
}
void _build(double shrinkOffset, bool overlapsContent) {
owner!.buildScope(this, () {
final _SliverPersistentHeaderRenderObjectWidget sliverPersistentHeaderRenderObjectWidget = widget as _SliverPersistentHeaderRenderObjectWidget;
child = updateChild(
child,
floating
? _FloatingHeader(child: sliverPersistentHeaderRenderObjectWidget.delegate.build(
this,
shrinkOffset,
overlapsContent
))
: sliverPersistentHeaderRenderObjectWidget.delegate.build(this, shrinkOffset, overlapsContent),
null,
);
_updateChild(sliverPersistentHeaderRenderObjectWidget.delegate, shrinkOffset, overlapsContent);
});
}

View File

@ -2274,6 +2274,53 @@ void main() {
expect(navToolBar.middleSpacing, NavigationToolbar.kMiddleSpacing);
});
// Regression test for https://github.com/flutter/flutter/issues/158158.
testWidgets('SliverAppBar should update TabBar before TabBar build', (WidgetTester tester) async {
final List<Tab> tabs = <Tab>[];
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return DefaultTabController(
length: tabs.length,
child: Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
actions: <Widget>[
TextButton(
child: const Text('Add Tab'),
onPressed: () {
setState(() {
tabs.add(Tab(text: 'Tab ${tabs.length + 1}'));
});
},
),
],
bottom: TabBar(tabs: tabs),
),
],
),
),
);
},
),
),
);
// Initializes with zero tabs.
expect(find.text('Tab 1'), findsNothing);
expect(find.text('Tab 2'), findsNothing);
// No crash after tabs added.
await tester.tap(find.text('Add Tab'));
await tester.pumpAndSettle();
expect(find.text('Tab 1'), findsOneWidget);
expect(find.text('Tab 2'), findsNothing);
expect(tester.takeException(), isNull);
});
group('Material 2', () {
// These tests are only relevant for Material 2. Once Material 2
// support is deprecated and the APIs are removed, these tests