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

diff --git a/packages/flutter/lib/src/material/tab_controller.dart b/packages/flutter/lib/src/material/tab_controller.dart
index f4813c3..b8073a1 100644
--- a/packages/flutter/lib/src/material/tab_controller.dart
+++ b/packages/flutter/lib/src/material/tab_controller.dart
@@ -130,10 +130,9 @@
   /// 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 @@
   @override
   void dispose() {
     _animationController?.dispose();
+    _animationController = null;
     super.dispose();
   }
 }
diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart
index c4b5e1f..aedd016 100644
--- a/packages/flutter/lib/src/material/tabs.dart
+++ b/packages/flutter/lib/src/material/tabs.dart
@@ -458,6 +458,18 @@
   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);
     return (controller.animation.value - index.toDouble()).abs().clamp(0.0, 1.0);
@@ -768,6 +780,11 @@
     );
   }
 
+  // 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 @@
     if (newController == _controller)
       return;
 
-    if (_controller != null) {
+    if (_controllerIsValid) {
       _controller.animation.removeListener(_handleTabControllerAnimationTick);
       _controller.removeListener(_handleTabControllerTick);
     }
@@ -799,7 +816,7 @@
   }
 
   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 @@
   @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 @@
   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 @@
     if (newController == _controller)
       return;
 
-    if (_controller != null)
+    if (_controllerIsValid)
       _controller.animation.removeListener(_handleTabControllerAnimationTick);
     _controller = newController;
     if (_controller != null)
@@ -1179,8 +1202,9 @@
 
   @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();
   }
diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart
index 8677c5e..dfbefe3 100644
--- a/packages/flutter/test/material/tabs_test.dart
+++ b/packages/flutter/test/material/tabs_test.dart
@@ -2299,4 +2299,55 @@
     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);
+  });
 }