Skip to content

TrainHoppingAnimation Calls notifyListeners() on Status Change Instead of notifyStatusListeners() #178336

Closed

Description

@swrenn

In its animation status listener, TrainHoppingAnimation calls notifyListeners() on status change when it should call notifyStatusListeners().

AnimationStatus? _lastStatus;
void _statusChangeHandler(AnimationStatus status) {
assert(_currentTrain != null);
if (status != _lastStatus) {
notifyListeners();
_lastStatus = status;
}
assert(_lastStatus != null);
}

Code sample

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

void main() {
  runApp(const App());
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Screen(),
    );
  }
}

class Screen extends StatefulWidget {
  const Screen({super.key});

  @override
  State<Screen> createState() => _ScreenState();
}

class _ScreenState extends State<Screen> with SingleTickerProviderStateMixin {
  late final _controller = AnimationController(
    vsync: this,
    duration: const Duration(seconds: 3),
  );

  late final _animation = TrainHoppingAnimation(
    Tween<double>(begin: 1, end: 0).animate(_controller),
    Tween<double>(begin: 0, end: 1).animate(_controller),
  );

  @override
  void initState() {
    super.initState();
    _controller.addStatusListener((status) {
      print('controller: $status');
    });
    _animation.addStatusListener((status) {
      print('animation: $status');
    });
    _controller.forward();
  }

  @override
  void dispose() {
    _animation.dispose();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ScaleTransition(
        scale: _animation,
        child: Container(
          color: Colors.red,
          height: 200,
          width: 200,
        ),
      ),
    );
  }
}

Activity

  1. darshankawar commented on Nov 12, 2025

    @darshankawar
    Member

    @swrenn Can you provide us a code example that shows the current behavior versus expected behavior ?
    Below is a sample code based on the issue you described it shortly:

    runnable code
    
    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const AnimationApp());
    }
    
    class AnimationApp extends StatelessWidget {
      const AnimationApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Train Hopping Animation',
          theme: ThemeData(
            useMaterial3: true,
            primarySwatch: Colors.blueGrey,
          ),
          home: const TrainHoppingScreen(),
        );
      }
    }
    
    // 1. Define the Stateful Widget and Ticker Provider
    class TrainHoppingScreen extends StatefulWidget {
      const TrainHoppingScreen({super.key});
    
      @override
      State<TrainHoppingScreen> createState() => _TrainHoppingScreenState();
    }
    
    // 2. Use SingleTickerProviderStateMixin for the AnimationController
    class _TrainHoppingScreenState extends State<TrainHoppingScreen> with SingleTickerProviderStateMixin {
      late AnimationController _controller;
      late Animation<double> _hopAnimation;
    
      String _statusMessage = 'Initializing...';
    
      @override
      void initState() {
        super.initState();
    
        // Initialize the AnimationController
        _controller = AnimationController(
          vsync: this,
          duration: const Duration(milliseconds: 800), // Time for one hop
        );
    
        // Create the animation: a vertical movement from 0.0 to -100.0 (upwards hop)
        // The value 100.0 is used as a multiplier for the hop distance.
        _hopAnimation = Tween<double>(begin: 0.0, end: -100.0).animate(
          // Apply a smooth bounce curve to the animation
          CurvedAnimation(
            parent: _controller,
            curve: Curves.easeOut,
          ),
        );
    
        // 3. Add Status Listener to the AnimationController
        // This is the core requirement: listening to the animation status.
        _controller.addStatusListener((status) {
          if (status == AnimationStatus.completed) {
            // When the hop is complete (reached the peak), reverse the animation
            _controller.reverse();
            setState(() {
              _statusMessage = 'Hop Completed. Reversing...';
            });
          } else if (status == AnimationStatus.dismissed) {
            // When the hop is dismissed (back on the ground), repeat the animation
            _controller.forward();
            setState(() {
              _statusMessage = 'Landed. Starting next Hop!';
            });
          } else if (status == AnimationStatus.forward) {
            setState(() {
              _statusMessage = 'Hopping Forward...';
            });
          } else if (status == AnimationStatus.reverse) {
            setState(() {
              _statusMessage = 'Descending (Reverse)...';
            });
          }
          // Print to console to show real-time status changes
          print('Animation Status: $status');
        });
    
        // Start the animation immediately
        _controller.forward();
      }
    
      @override
      void dispose() {
        // Clean up the controller when the widget is disposed
        _controller.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Train Hopper Demo'),
            backgroundColor: Colors.blueGrey,
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                // Display the current animation status
                Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Text(
                    'Status: $_statusMessage',
                    style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
                    textAlign: TextAlign.center,
                  ),
                ),
                const SizedBox(height: 50),
    
                // 4. Use AnimatedBuilder to apply the animation value
                AnimatedBuilder(
                  animation: _hopAnimation,
                  builder: (context, child) {
                    // Apply a vertical translation based on the animation value
                    return Transform.translate(
                      offset: Offset(0, _hopAnimation.value),
                      child: child,
                    );
                  },
                  child: Container(
                    width: 150,
                    height: 80,
                    decoration: BoxDecoration(
                      color: Colors.red[700],
                      borderRadius: BorderRadius.circular(12),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black.withOpacity(0.3),
                          spreadRadius: 2,
                          blurRadius: 5,
                          offset: const Offset(0, 5),
                        ),
                      ],
                    ),
                    child: const Center(
                      child: Text(
                        'The Hopper',
                        style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
                      ),
                    ),
                  ),
                ),
                const SizedBox(height: 10),
                // "Train" element to hop next to
                Container(
                  width: 250,
                  height: 50,
                  decoration: BoxDecoration(
                    color: Colors.blueGrey[900],
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: const Center(
                    child: Text(
                      'The Train ',
                      style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    

    which prints below:

    flutter: Animation Status: AnimationStatus.forward
    flutter: Animation Status: AnimationStatus.reverse
    flutter: Animation Status: AnimationStatus.completed
    flutter: Animation Status: AnimationStatus.forward
    flutter: Animation Status: AnimationStatus.dismissed
    flutter: Animation Status: AnimationStatus.reverse
    flutter: Animation Status: AnimationStatus.completed
    flutter: Animation Status: AnimationStatus.forward
    

    Please check if above correctly interprets the issue you are reporting or please update it to show current behavior as compared to expected.

  2. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Nov 12, 2025
  3. self-assigned this
    on Nov 12, 2025
  4. moved this from To do to In progress in Nevercodeon Nov 12, 2025
  5. ksokolovskyi commented on Nov 12, 2025

    @ksokolovskyi
    Contributor

    Hi @darshankawar, thanks a lot for taking a look at this issue. I've added a code sample to the issue. May I ask you to try it?

  6. darshankawar commented on Nov 12, 2025

    @darshankawar
    Member

    Thanks @ksokolovskyi for the code. With it, I get below which doesn't seem to notify all statuses.

    flutter: controller: AnimationStatus.forward
    flutter: controller: AnimationStatus.completed
    

    While https://api.flutter.dev/flutter/animation/AnimationLocalStatusListenersMixin/notifyStatusListeners.html does mention that it calls all status listeners, so ideally it should be called instead of notifyListeners.

  7. added
    frameworkflutter/packages/flutter repository. See also f: labels.
    and removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    in triagePresently being triaged by the triage team
    on Nov 12, 2025
  8. moved this from In progress to PR submitted in Nevercodeon Nov 17, 2025
  9. moved this from PR submitted to Done (PR merged) in Nevercodeon Nov 19, 2025
  10. added
    r: fixedIssue is closed as already fixed in a newer version
    on Nov 19, 2025
  11. github-actions commented on Dec 3, 2025

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  12. locked as resolved and limited conversation to collaborators on Dec 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

a: animationAnimation APIsframeworkflutter/packages/flutter repository. See also f: labels.r: fixedIssue is closed as already fixed in a newer versionteam-frameworkOwned by Framework team

Type

No type

Projects

  • Status
    Done (PR merged)

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions