In Flutter, when do you use CurvedAnimation rather than CurveTween, and what does CurvedAnimation's reverseCurve change?
answer
- Animation versus Animatable again
- status listener on the parent
- create in initState, dispose it
- reverseCurve for runs started in reverse
- mid-flight flip keeps the current curve
basics
~20 sCurveTween is a stateless Animatable you chain into a pipeline and never dispose. CurvedAnimation is an Animation that listens to its parent's status so it can use a separate reverseCurve on the way back, which is why it must be disposed.
solid answer
~40 sBoth apply a `Curve` to a linear 0-to-1 parent. `CurveTween(curve:)` is an `Animatable<double>`: no listeners, no state, nothing to dispose, and it composes with `chain` or `drive`. `CurvedAnimation(parent:, curve:, reverseCurve:)` is itself an `Animation<double>`; its constructor adds a status listener to the parent so it knows the direction and can apply `reverseCurve` to a run that starts in reverse. If the parent flips direction mid-flight, without reaching `completed` or `dismissed`, it stays on the curve it started with, to avoid a jump. Because of that listener it belongs in `initState` with a matching `dispose()`. The framework's own advice: if you do not need `reverseCurve`, prefer `parent.drive(CurveTween(curve: ...))`.
code
dart · 41 linesimport 'package:flutter/material.dart';
class FilterPanel extends StatefulWidget {
const FilterPanel({super.key, required this.child});
final Widget child;
@override
State<FilterPanel> createState() => _FilterPanelState();
}
class _FilterPanelState extends State<FilterPanel>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final CurvedAnimation _curved;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_curved = CurvedAnimation(
parent: _controller,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeOutCubic.flipped,
);
}
@override
void dispose() {
_curved.dispose();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizeTransition(sizeFactor: _curved, child: widget.child);
}
}go deeper
Recall that both classes apply a curve to a controller, and that CurvedAnimation is the one with reverseCurve and a dispose method.
Explain the status listener CurvedAnimation adds to its parent, why that forces creation in initState and disposal, and how a mid-flight direction change keeps the current curve.
Default to drive with CurveTween, reserve CurvedAnimation for a distinct reverse curve, and catch CurvedAnimation constructed in build during review before it leaks listeners.
Weigh a team convention of stateless CurveTween pipelines against the occasional need for direction-aware curves, and decide where the disposal discipline is enforced.
## Two ways to bend a linear timeline An `AnimationController` advances linearly: halfway through its `duration`, its value is `0.5`. A **curve** (`Curve`) reshapes that progress - `Curves.easeOut` covers most of the distance early and slows into the end. Flutter offers two objects that apply a curve to a parent `Animation<double>`, and they are not interchangeable: **`CurveTween`** and **`CurvedAnimation`**. ## CurveTween: a stateless step in a pipeline `CurveTween(curve: ...)` is an **`Animatable<double>`**, the same family as `Tween`. It has a mutable `curve` field and a `transform(t)` that returns `curve.transform(t)`, passing `0.0` and `1.0` straight through. It holds no reference to any parent, registers no listener and keeps no state, so: - it composes with `drive` and `chain`: `controller.drive(CurveTween(curve: Curves.easeOut)).drive(Tween<double>(begin: 0, end: 240))`; - it can sit inside a `TweenSequenceItem` through `tween.chain(CurveTween(...))`; - it **never needs disposing**, and one instance can be shared by many pipelines. ## CurvedAnimation: an Animation that knows the direction `CurvedAnimation(parent: ..., curve: ..., reverseCurve: ...)` is an **`Animation<double>`** in its own right. Its constructor calls `parent.addStatusListener(...)` so it can track which way the parent is running. That buys one capability `CurveTween` lacks - a **separate curve for the reverse direction** - and costs one obligation: it must be created once and **disposed**. Its `dispose()` removes the status listener and sets `isDisposed`. The class also reports its creation and disposal to Flutter's leak-tracking hooks, so an instance that is never disposed can be flagged. ## What reverseCurve actually does The rules, as `CurvedAnimation` implements them: 1. If `reverseCurve` is `null`, `curve` is used in both directions. Playing a curve backwards mirrors its feel: an ease-out curve run in reverse starts slowly and speeds up toward `0.0`. 2. If `reverseCurve` is set, it is used for a run that **starts** while the parent's status is `AnimationStatus.reverse`. 3. If the parent changes direction **mid-flight**, without first reaching `completed` or `dismissed`, the animation keeps the curve it started with, now traversed the other way. Switching curves at that instant would make the value jump, because two curves give different outputs for the same `t`. 4. The remembered direction clears when the parent reaches `completed` or `dismissed`, so the next run picks its curve afresh. A common pattern is `reverseCurve: curve.flipped`. `Curve.flipped` returns a `FlippedCurve`, the curve rotated half a turn, so the return trip decelerates into `0.0` the way the forward trip decelerated into `1.0`. Material's `NavigationBar` passes `Curves.easeInOutCubicEmphasized.flipped` as a `reverseCurve`. ## Lifecycle in a State The shape is always the same: create the controller, then the `CurvedAnimation`, in `initState`; dispose the `CurvedAnimation` before the controller in `dispose`. ```dart _curved = CurvedAnimation( parent: _controller, curve: Curves.easeOutCubic, reverseCurve: Curves.easeOutCubic.flipped, ); // ... @override void dispose() { _curved.dispose(); _controller.dispose(); super.dispose(); } ``` The mistake interviewers probe is constructing a `CurvedAnimation` inside `build()`. Every construction adds a status listener to the long-lived controller, and every rebuild leaves another one behind. The framework's doc comment says exactly this: create it once, dispose it, and do not recreate it on every build. ## Choosing between them | Need | Use | |---|---| | One curve in both directions | `controller.drive(CurveTween(curve: c))` | | A curve inside `chain` or a `TweenSequence` | `CurveTween` | | A different curve on the way back | `CurvedAnimation` with `reverseCurve` | | An `Animation<double>` to hand to a transition widget | either - `drive` also returns an `Animation` | In practice, reach for `CurveTween` by default and promote to `CurvedAnimation` only when the reverse direction needs its own curve. ## Mistakes interviewers listen for 1. Saying the two classes are interchangeable, then building `CurvedAnimation` inside `build()`. 2. Disposing the controller but forgetting the `CurvedAnimation`, whose status listener was registered on that controller. 3. Expecting `reverseCurve` to take over the moment the controller turns around mid-flight. 4. Assuming that, without `reverseCurve`, the reverse trip feels identical to the forward one - it is the same curve traversed backwards, so its easing is mirrored in time. 5. Wrapping a `CurveTween` in extra lifecycle code it does not need.
- What happens if the controller reverses at 0.6, partway through a forward run, when reverseCurve is set?`CurvedAnimation` remembers the direction the run started in and only clears it when the parent reaches `completed` or `dismissed`. So it keeps applying the forward `curve`, now traversed backwards, until the controller settles at 0.0. Switching to `reverseCurve` at 0.6 would make the value jump, because the two curves give different outputs for the same `t`.
- Why is creating a CurvedAnimation inside build() a bug?Each construction adds a status listener to the parent controller. Building a new `CurvedAnimation` on every rebuild, with none of them disposed, piles listeners onto the controller for as long as it lives. Create it once in `initState` and dispose it in `dispose`, or, when no `reverseCurve` is needed, use `drive(CurveTween(...))`, which registers nothing.
saying these in an interview costs you the question
- CurveTween and CurvedAnimation are interchangeable in every situation.
- CurveTween must be disposed just like CurvedAnimation.
- Building a CurvedAnimation inside build() is harmless because it is cheap.
- reverseCurve takes over the moment the controller changes direction mid-flight.
- Without reverseCurve, the reverse run eases exactly like the forward run.