skip to content

In Flutter's animation library, what does a Tween do, and how does Tween.animate() relate to Animation.drive()?

level: juniorimportance: must knowfreq 62%

answer

  1. controller only emits doubles
  2. begin, end and lerp
  3. Animatable, not an Animation
  4. drive calls animate from the other side
  5. ColorTween for Color, IntTween for int

basics

~10 s

A Tween is an Animatable that maps a 0.0-to-1.0 animation value onto a begin-to-end range of sizes, colors or offsets. tween.animate(controller) and controller.drive(tween) are the same operation written from opposite ends.

solid answer

~30 s

An `AnimationController` only produces doubles, by default from 0.0 to 1.0. A `Tween<T>` holds `begin` and `end` and implements `lerp(t)`, so `transform(0.0)` is `begin`, `transform(1.0)` is `end`, and anything between is interpolated. A tween is an `Animatable`, not an `Animation`: it has no listeners and no clock. You bind it with `tween.animate(controller)` or `controller.drive(tween)` - `drive` simply calls `child.animate(this)` - and get an `Animation<T>` whose `value` is computed each time it is read. Types without usable `+`, `-` and `*` need a dedicated subclass: `ColorTween` (via `Color.lerp`), `SizeTween`, `RectTween`, `IntTween` or `StepTween`, `AlignmentTween`.

code

dart · 45 lines
dart
import 'package:flutter/material.dart';

class SwatchState extends State<Swatch> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _width;
  late final Animation<Color?> _color;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 400),
    );
    _width = Tween<double>(begin: 80, end: 240).animate(_controller);
    _color = _controller.drive(
      ColorTween(begin: Colors.teal, end: Colors.deepOrange),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, Widget? child) => Container(
        width: _width.value,
        height: 48,
        color: _color.value,
      ),
    );
  }
}

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

  @override
  State<Swatch> createState() => SwatchState();
}

go deeper

for a junior

Recall that the controller produces 0.0 to 1.0 and a Tween maps that onto a begin and end of the type the widget needs. Know both the animate and drive spellings.

for a middle

Explain the Animatable versus Animation split, that drive calls animate under the hood, and why Color and int need ColorTween, IntTween or StepTween rather than a plain Tween.

for a senior

Show that you retarget a running animation by mutating a tween's begin and end, keep constant tweens in static final fields, and never rebuild animation pipelines inside build.

for a principal

Treat tweens as pure, stateless mappings a team can share and compose, and argue for a small set of typed tweens over ad hoc arithmetic scattered through builder callbacks.

## What an AnimationController gives you, and what it does not An **`AnimationController`** is Flutter's clock-driven animation source. With its default bounds it produces a `double` that moves from `0.0` to `1.0` over its `duration`, updating once per frame while it runs. That number is rarely what a widget wants: a card needs a width in logical pixels, a banner needs a `Color`, a panel needs an `Offset`. Something has to translate *progress* into *a value of the right type*. That translator is a **tween**. ## Tween is an Animatable, not an Animation Flutter's animation library separates two roles: - **`Animation<T>`** - a `Listenable` with a current `value` and an `AnimationStatus`; widgets subscribe to it. `AnimationController` is one. - **`Animatable<T>`** - a pure mapping from a `double` `t` to a `T`, through `transform(t)`. It has no listeners, no status and no clock. `Tween<T>` is the main `Animatable`. It stores two mutable fields, `begin` and `end`, and implements `lerp(t)` (linear interpolation). Its `transform` returns `begin` exactly at `t == 0.0`, `end` exactly at `t == 1.0`, and `lerp(t)` everywhere else. The default `lerp` computes `begin + (end - begin) * t`, which is why a plain `Tween<double>` or `Tween<Offset>` works: `double` and `Offset` define `+`, `-` and `*` with the right result types. Because a tween is not an `Animation`, you cannot listen to it or call `forward()` on it. You have to **bind** it to an `Animation<double>`. ## animate() and drive(): one operation, two spellings There are two ways to bind, and they build the same kind of object: ```dart // Both produce an Animation<double> that reads 0 -> 300 as the controller runs. final Animation<double> a = Tween<double>(begin: 0, end: 300).animate(_controller); final Animation<double> b = _controller.drive(Tween<double>(begin: 0, end: 300)); ``` `Animation.drive(child)` is implemented as `child.animate(this)`, and it asserts that the receiver is an `Animation<double>`. The returned animation holds a reference to its parent and to the tween and **computes its value on every read** - nothing is precomputed per frame. Choosing between the two is a matter of reading order: | Spelling | Reads as | Typical use | |---|---|---| | `tween.animate(controller)` | start from the value range | one tween stored as a field | | `controller.drive(tween)` | start from the clock | a pipeline: `controller.drive(CurveTween(...)).drive(Tween(...))` | `drive` reads left to right when several steps are stacked, which is why the framework's own samples use it for curve-then-tween pipelines. ## Typed tweens: when Tween<T> is not enough Some types lack the arithmetic operators, or have them with the wrong result type. For those, Flutter ships dedicated subclasses that override `lerp`: | Type | Tween class | How it interpolates | |---|---|---| | `Color?` | `ColorTween` | `Color.lerp`, channel by channel, clamped to 0-1 | | `Size?` | `SizeTween` | `Size.lerp` | | `Rect?` | `RectTween` | `Rect.lerp` | | `int` | `IntTween` / `StepTween` | rounds / floors a double result | | `Alignment` | `AlignmentTween` | `Alignment.lerp` | | any `T` | `ConstantTween<T>` | always returns the one value | In a debug build, a `Tween<Color>` fails at the first interpolated frame with a `FlutterError` whose summary reads *Cannot lerp between* and whose hint suggests `ColorTween`. A `Tween<int>` fails with a hint to use `IntTween` or `StepTween`, because `int * double` does not produce an `int`. `ColorTween` has one subtlety worth knowing: its ends may be `null`, meaning *no color*. Fading `Colors.red` to `null` scales only red's alpha. Fading to `Colors.transparent` - which is transparent **black** - also pulls the red, green and blue channels toward black on the way out, so the color muddies as it fades. ## Mutability and reuse `begin` and `end` are ordinary mutable fields. An animation built with `animate()` or `drive()` honours a change to them immediately, so you can retarget a running animation without rebuilding the chain from the controller down; listeners hear the change on the next tick. A tween whose values never change can live in a `static final` field; recreating an identical tween in every `build()` is avoidable garbage. A custom tween overrides **`lerp`**, not `transform` or `evaluate`, because the other methods are defined in terms of it. ## Mistakes interviewers listen for 1. Calling a tween an animation, or expecting to `forward()` it. 2. Believing `animate()` and `drive()` behave differently. 3. Reaching for `Tween<Color>` or `Tween<int>` instead of the dedicated classes. 4. Using `Colors.transparent` where `null` was meant in a `ColorTween`. 5. Overriding `transform` in a custom tween and losing the exact-endpoint guarantee.

  • Why does Tween<Color>(begin: Colors.red, end: Colors.blue) fail, and what should you use?
    Tween's default `lerp` computes `begin + (end - begin) * t` through dynamic operators, and `Color` defines none of them. In a debug build the first mid-animation frame throws a `FlutterError` saying it cannot lerp, with a hint to use `ColorTween`, which delegates to `Color.lerp`. `int` has a related trap: `int * double` is not an `int`, so use `IntTween` or `StepTween`.
  • In a ColorTween, what is the difference between end: null and end: Colors.transparent?
    `ColorTween` treats `null` as no color: `Color.lerp` then scales only the other color's alpha, so red fades out while staying red. `Colors.transparent` is transparent black, so the interpolation also drags the channels toward black and the color darkens as it fades. The framework's own doc comment recommends `null` for fading to or from transparent.
  • Can you change a Tween's begin or end after calling animate()?
    Yes. `begin` and `end` are mutable fields, and the Animation returned by `animate()` or `drive()` evaluates the tween each time its `value` is read, so the new range applies immediately; listeners are notified only while the animation is ticking. Tweens that never change can sit in a `static final` field instead of being recreated in `build()`.

saying these in an interview costs you the question

  • A Tween is itself an Animation you can call forward() on.
  • animate() and drive() produce different kinds of animation.
  • Tween<Color> works because Flutter interpolates any type generically.
  • animate() precomputes the tween's value for every frame up front.
  • Colors.transparent and null give the same fade in a ColorTween.