I have an UI with a TabBar and a button on each screen (I'm using the package ScaleTap for the button https://pub.dev/packages/flutter_scale_tap)
I noticed that when I swipe through the pages on the TabBar very quickly, I get the following error:
AnimationController.stop() called after AnimationController.dispose() AnimationController methods should not be used after calling dispose. 'package:flutter/src/animation/animation_controller.dart': Failed assertion: line 772 pos 7: '_ticker != null'
I couldn't get to the bottom of this. The error is not that easy to replicate because you have to swipe very fast for it to happen (it only occurs when I swipe with three fingers to skip the TabBar pages very quickly). This doesn't seem to actually affect the usage of the app in practice, but I'm curious why that happens and if there's a way to fix the error.
Any ideas on what's happening here?
`
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_scale_tap/flutter_scale_tap.dart';
class Events extends StatefulWidget {
const Events({Key? key}) : super(key: key);
@override
_EventsState createState() => _EventsState();
}
class _EventsState extends State<Events> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
body: AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.dark,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 60.0,),
const Padding(
padding: EdgeInsets.only(left: 31.0),
child: Text(
'My page',
style: TextStyle(
fontSize: 22,
color: Color(0xff101010),
fontWeight: FontWeight.w700,
),
),
),
const TabBar(
indicatorColor: Color(0xFF101010),
labelColor: Color(0xFF101010),
unselectedLabelColor: Color(0xFF7E7E7E),
indicatorSize: TabBarIndicatorSize.label,
indicatorWeight: 1.0,
// labelPadding: EdgeInsets.all(0),
padding: EdgeInsets.only(top: 20.0, bottom: 5.0),
indicatorPadding: EdgeInsets.only(bottom: 8.0),
tabs: [
Tab(
child: Text(
"1",
style: TextStyle(
fontSize: 16,
),
),
),
Tab(
child: Text(
"2",
style: TextStyle(
fontSize: 16,
),
),
),
Tab(
child: Text(
"3",
style: TextStyle(
fontSize: 16,
),
),
),
],
),
Expanded(
child: TabBarView(
children: [
SizedBox(
child: Center(
child: ScaleTap(
onPressed: () {},
child: Container(
width: 200.0,
height: 300.0,
color: Colors.red,
),
),
),
),
SizedBox(
child: Center(
child: ScaleTap(
onPressed: () {},
child: Container(
width: 200.0,
height: 300.0,
color: Colors.red,
),
),
),
),
SizedBox(
child: Center(
child: ScaleTap(
onPressed: () {},
child: Container(
width: 200.0,
height: 300.0,
color: Colors.red,
),
),
),
),
]
),
),
],
),
),
),
);
}
}
`
I'm gessing this is a problem with the package ScaleTap, but I looked into the source code and couldn't understand what causes this error.
I recently stumbled upon this problem as well. The AnimationController gets disposed when switching tabs while the animation is still in progress. I copied over the source code of the ScaleTap class and made some changes.
I added a bool "isDisposed" which defaults to false; In the dispose() method i set that bool to false; Lastly i return an empty Future instead of calling the anim() methods if isDisposed is false.
You can check the code below. I added comments next to the lines I added.
Cheers!
library flutter_scale_tap;
import 'package:flutter/physics.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
const double _DEFAULT_SCALE_MIN_VALUE = 0.95;
const double _DEFAULT_OPACITY_MIN_VALUE = 0.90;
final Curve _DEFAULT_SCALE_CURVE = CurveSpring(); // ignore: non_constant_identifier_names
const Curve _DEFAULT_OPACITY_CURVE = Curves.ease;
const Duration _DEFAULT_DURATION = Duration(milliseconds: 300);
class ScaleTapConfig {
static double? scaleMinValue;
static Curve? scaleCurve;
static double? opacityMinValue;
static Curve? opacityCurve;
static Duration? duration;
}
class ScaleTap extends StatefulWidget {
final Function()? onPressed;
final Function()? onLongPress;
final Widget? child;
final Duration? duration;
final double? scaleMinValue;
final Curve? scaleCurve;
final Curve? opacityCurve;
final double? opacityMinValue;
final bool enableFeedback;
ScaleTap({
this.enableFeedback = true,
this.onPressed,
this.onLongPress,
required this.child,
this.duration,
this.scaleMinValue,
this.opacityMinValue,
this.scaleCurve,
this.opacityCurve,
});
@override
_ScaleTapState createState() => _ScaleTapState();
}
class _ScaleTapState extends State<ScaleTap> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scale;
late Animation<double> _opacity;
bool isDisposed = false; // Added this line
@override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this);
_scale = Tween<double>(begin: 1.0, end: 1.0).animate(_animationController);
_opacity = Tween<double>(begin: 1.0, end: 1.0).animate(_animationController);
}
@override
void dispose() {
isDisposed = true; /// Added this line
_animationController.dispose();
super.dispose();
}
Future<void> anim({double? scale, double? opacity, Duration? duration}) {
_animationController.stop();
_animationController.duration = duration ?? Duration.zero;
_scale = Tween<double>(
begin: _scale.value,
end: scale,
).animate(CurvedAnimation(
curve: widget.scaleCurve ?? ScaleTapConfig.scaleCurve ?? _DEFAULT_SCALE_CURVE,
parent: _animationController,
));
_opacity = Tween<double>(
begin: _opacity.value,
end: opacity,
).animate(CurvedAnimation(
curve: widget.opacityCurve ?? ScaleTapConfig.opacityCurve ?? _DEFAULT_OPACITY_CURVE,
parent: _animationController,
));
_animationController.reset();
return _animationController.forward();
}
Future<void> _onTapDown(_) {
if(isDisposed) return Future<void>(() {}); // Added this line
return anim(
scale: widget.scaleMinValue ?? ScaleTapConfig.scaleMinValue ?? _DEFAULT_SCALE_MIN_VALUE,
opacity: widget.opacityMinValue ?? ScaleTapConfig.opacityMinValue ?? _DEFAULT_OPACITY_MIN_VALUE,
duration: widget.duration ?? ScaleTapConfig.duration ?? _DEFAULT_DURATION,
);
}
Future<void> _onTapUp(_) {
if(isDisposed) return Future<void>(() {}); // Added this line
return anim(
scale: 1.0,
opacity: 1.0,
duration: widget.duration ?? ScaleTapConfig.duration ?? _DEFAULT_DURATION,
);
}
Future<void> _onTapCancel(_) {
return _onTapUp(_);
}
@override
Widget build(BuildContext context) {
final bool isTapEnabled = widget.onPressed != null || widget.onLongPress != null;
return AnimatedBuilder(
animation: _animationController,
builder: (_, Widget? child) {
return Opacity(
opacity: _opacity.value,
child: Transform.scale(
alignment: Alignment.center,
scale: _scale.value,
child: child,
),
);
},
child: Listener(
onPointerDown: isTapEnabled ? _onTapDown : null,
onPointerCancel: _onTapCancel,
onPointerUp: _onTapUp,
child: GestureDetector(
onTap: isTapEnabled
? () {
if (widget.enableFeedback) {
SystemSound.play(SystemSoundType.click);
}
widget.onPressed?.call();
}
: null,
onLongPress: isTapEnabled ? widget.onLongPress : null,
child: widget.child,
),
),
);
}
}
class CurveSpring extends Curve {
final SpringSimulation sim;
CurveSpring() : this.sim = _sim(70, 20);
@override
double transform(double t) => sim.x(t) + t * (1 - sim.x(1.0));
}
_sim(double stiffness, double damping) => SpringSimulation(
SpringDescription.withDampingRatio(
mass: 1,
stiffness: stiffness,
ratio: 0.7,
),
0.0,
1.0,
0.0,
);