Unverified Commit 6d0e235c authored by Hans Muller's avatar Hans Muller Committed by GitHub

Support for replacing the TabController, after disposing the old one (#32434)

parent 3fb1079a
......@@ -130,10 +130,9 @@ class TabController extends ChangeNotifier {
/// animation's value can be [offset] by +/- 1.0 to reflect [TabBarView]
/// drag scrolling.
///
/// If length is zero or one, [index] animations don't happen and the value
/// of this property is [kAlwaysCompleteAnimation].
Animation<double> get animation => _animationController?.view ?? kAlwaysCompleteAnimation;
final AnimationController _animationController;
/// If the TabController was disposed then return null.
Animation<double> get animation => _animationController?.view;
AnimationController _animationController;
/// The total number of tabs. Typically greater than one. Must match
/// [TabBar.tabs]'s and [TabBarView.children]'s length.
......@@ -221,6 +220,7 @@ class TabController extends ChangeNotifier {
@override
void dispose() {
_animationController?.dispose();
_animationController = null;
super.dispose();
}
}
......
......@@ -457,6 +457,18 @@ class _DragAnimation extends Animation<double> with AnimationWithParentMixin<dou
@override
Animation<double> get parent => controller.animation;
@override
void removeStatusListener(AnimationStatusListener listener) {
if (controller.animation != null)
super.removeStatusListener(listener);
}
@override
void removeListener(VoidCallback listener) {
if (controller.animation != null)
super.removeListener(listener);
}
@override
double get value {
assert(!controller.indexIsChanging);
......@@ -768,6 +780,11 @@ class _TabBarState extends State<TabBar> {
);
}
// If the TabBar is rebuilt with a new tab controller, the caller should
// dispose the old one. In that case the old controller's animation will be
// null and should not be accessed.
bool get _controllerIsValid => _controller?.animation != null;
void _updateTabController() {
final TabController newController = widget.controller ?? DefaultTabController.of(context);
assert(() {
......@@ -786,7 +803,7 @@ class _TabBarState extends State<TabBar> {
if (newController == _controller)
return;
if (_controller != null) {
if (_controllerIsValid) {
_controller.animation.removeListener(_handleTabControllerAnimationTick);
_controller.removeListener(_handleTabControllerTick);
}
......@@ -799,7 +816,7 @@ class _TabBarState extends State<TabBar> {
}
void _initIndicatorPainter() {
_indicatorPainter = _controller == null ? null : _IndicatorPainter(
_indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter(
controller: _controller,
indicator: _indicator,
indicatorSize: widget.indicatorSize ?? TabBarTheme.of(context).indicatorSize,
......@@ -840,10 +857,11 @@ class _TabBarState extends State<TabBar> {
@override
void dispose() {
_indicatorPainter.dispose();
if (_controller != null) {
if (_controllerIsValid) {
_controller.animation.removeListener(_handleTabControllerAnimationTick);
_controller.removeListener(_handleTabControllerTick);
}
_controller = null;
// We don't own the _controller Animation, so it's not disposed here.
super.dispose();
}
......@@ -1129,6 +1147,11 @@ class _TabBarViewState extends State<TabBarView> {
int _currentIndex;
int _warpUnderwayCount = 0;
// If the TabBarView is rebuilt with a new tab controller, the caller should
// dispose the old one. In that case the old controller's animation will be
// null and should not be accessed.
bool get _controllerIsValid => _controller?.animation != null;
void _updateTabController() {
final TabController newController = widget.controller ?? DefaultTabController.of(context);
assert(() {
......@@ -1147,7 +1170,7 @@ class _TabBarViewState extends State<TabBarView> {
if (newController == _controller)
return;
if (_controller != null)
if (_controllerIsValid)
_controller.animation.removeListener(_handleTabControllerAnimationTick);
_controller = newController;
if (_controller != null)
......@@ -1179,8 +1202,9 @@ class _TabBarViewState extends State<TabBarView> {
@override
void dispose() {
if (_controller != null)
if (_controllerIsValid)
_controller.animation.removeListener(_handleTabControllerAnimationTick);
_controller = null;
// We don't own the _controller Animation, so it's not disposed here.
super.dispose();
}
......
......@@ -2299,4 +2299,55 @@ void main() {
final IconThemeData iconTheme = IconTheme.of(tester.element(find.text('A')));
expect(iconTheme.color, equals(selectedTabColor));
});
testWidgets('Replacing the tabController after disposing the old one', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/32428
TabController controller = TabController(vsync: const TestVSync(), length: 2);
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: controller,
tabs: List<Widget>.generate(controller.length, (int index) => Tab(text: 'Tab$index')),
),
actions: <Widget>[
FlatButton(
child: const Text('Change TabController length'),
onPressed: () {
setState(() {
controller.dispose();
controller = TabController(vsync: const TestVSync(), length: 3);
});
},
),
],
),
body: TabBarView(
controller: controller,
children: List<Widget>.generate(controller.length, (int index) => Center(child: Text('Tab $index'))),
),
);
},
),
),
);
expect(controller.index, 0);
expect(controller.length, 2);
expect(find.text('Tab0'), findsOneWidget);
expect(find.text('Tab1'), findsOneWidget);
expect(find.text('Tab2'), findsNothing);
await tester.tap(find.text('Change TabController length'));
await tester.pumpAndSettle();
expect(controller.index, 0);
expect(controller.length, 3);
expect(find.text('Tab0'), findsOneWidget);
expect(find.text('Tab1'), findsOneWidget);
expect(find.text('Tab2'), findsOneWidget);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment