Skip to content

CupertinoSheetTransitionState creates multiple tickers / AnimationController dispose does not free tickers. #179337

@ElectricCookie

Description

@ElectricCookie

Steps to reproduce

When a CupertinoSheet updates (didUpdateWidget) it tries to dispose and re-create the animation. Since its using a SingleTickerProvider it fails to recreate the controller. The dispose method of an animation controller does not reset the TickerProvider.

Unsure which behaviour would be intended.

I would intuitively expect that I can re-use a ticker provider if I dispose the ticker correctly. So I would even go as far as saying the sheet implementation is sound, the ticker provider createTicker method could use a check to see if the previously created ticker was disposed.

Expected results

The animation is disposed correctly and recreated.

Actual results

Error occurs.

Code sample

I frankensteined this sample code to verify disposing an animation controller does not reset the provider mixin.

Code sample
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter Application',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(colorSchemeSeed: Colors.blue),
      home: const CounterScreen(title: 'Counter Screen'),
    );
  }
}

class CounterScreen extends StatefulWidget {
  final String title;

  const CounterScreen({super.key, required this.title});

  @override
  State<CounterScreen> createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> with SingleTickerProviderStateMixin {
  int _counter = 0;
  late AnimationController _animationController;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }


  /// Disposes the current [AnimationController] and creates a new one,
  /// vsynced to this [_CounterScreenState].
  void _disposeAndRecreateAnimationController() {
    _animationController.dispose(); // Dispose the existing controller
    _animationController = AnimationController(
      vsync: this, // Re-initialize with vsync to this
      duration: const Duration(milliseconds: 500),
    );
    
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    // A simple scale animation for the icon to demonstrate the controller is working
    final Animation<double> scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeOut,
      ),
    );

    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _disposeAndRecreateAnimationController,
              child: const Text('Reset Animation Controller'),
            ),
            const SizedBox(height: 20),
            // Display an icon that animates when _incrementCounter or _disposeAndRecreateAnimationController is called
            AnimatedBuilder(
              animation: _animationController,
              builder: (context, child) {
                return Transform.scale(
                  scale: scaleAnimation.value,
                  child: const Icon(Icons.fingerprint, size: 50, color: Colors.blue),
                );
              },
            ),
          ],
        ),
      ),
      
    );
  }
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
════════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown building LayoutBuilder:
_CupertinoSheetTransitionState is a SingleTickerProviderStateMixin but multiple tickers were created.
A SingleTickerProviderStateMixin can only be used as a TickerProvider once.
If a State is used for multiple AnimationController objects, or if it is passed to other objects and those objects might use it more than one time in total, then instead of mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.

The relevant error-causing widget was:
    LayoutBuilder LayoutBuilder:file:///Users/.../lib/src/modal/modal.dart:312:12

When the exception was thrown, this was the stack:
#0      SingleTickerProviderStateMixin.createTicker.<anonymous closure> (package:flutter/src/widgets/ticker_provider.dart:201:7)
ticker_provider.dart:201
#1      SingleTickerProviderStateMixin.createTicker (package:flutter/src/widgets/ticker_provider.dart:214:6)
ticker_provider.dart:214
#2      new AnimationController (package:flutter/src/animation/animation_controller.dart:257:21)
animation_controller.dart:257
#3      _CupertinoSheetTransitionState._setupAnimation (package:flutter/src/cupertino/sheet.dart:417:30)
sheet.dart:417
#4      _CupertinoSheetTransitionState.didUpdateWidget (package:flutter/src/cupertino/sheet.dart:396:7)

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.38.1, on macOS 26.2 25C5037j darwin-arm64, locale en-US) [503ms]
    • Flutter version 3.38.1 on channel stable at /Users/.../flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision b45fa18946 (3 weeks ago), 2025-11-12 22:09:06 -0600
    • Engine revision b5990e5ccc
    • Dart version 3.10.0
    • DevTools version 2.51.1
    • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets,
      omit-legacy-version-file, enable-lldb-debugging

[!] Android toolchain - develop for Android devices (Android SDK version 35.0.0) [1,840ms]
    • Android SDK at /Users/.../Library/Android/Sdk
    • Emulator version 36.1.9.0 (build_id 13823996) (CL:N/A)
    • Platform android-36, build-tools 35.0.0
    • ANDROID_SDK_ROOT = /Users/.../Library/Android/Sdk
    • Java binary at: /opt/homebrew/opt/openjdk@17/bin/java
      This JDK is specified in your Flutter configuration.
      To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment Homebrew (build 17.0.15+0)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses

[!] Xcode - develop for iOS and macOS (Xcode 26.0) [1,258ms]
    • Xcode at /Applications/Xcode-26.0.0.app/Contents/Developer
    • Build 17A324
    ! CocoaPods 1.15.2 out of date (1.16.2 is recommended).
        CocoaPods is a package manager for iOS or macOS platform code.
        Without CocoaPods, plugins will not work on iOS or macOS.
        For more info, see https://flutter.dev/to/platform-plugins
      To update CocoaPods, see https://guides.cocoapods.org/using/getting-started.html#updating-cocoapods

[✓] Chrome - develop for the web [6ms]
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work lista: error messageError messages from the Flutter frameworkf: cupertinoflutter/packages/flutter/cupertino repositoryfound in release: 3.38Found to occur in 3.38found in release: 3.39Found to occur in 3.39frameworkflutter/packages/flutter repository. See also f: labels.needs repro infoAutomated crash report whose cause isn't yet knownr: fixedIssue is closed as already fixed in a newer versionteam-designOwned by Design Languages teamtriaged-designTriaged by Design Languages team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions