skip to content

In Flutter, what does TweenAnimationBuilder add over the built-in implicit widgets, and what happens when its tween's end changes mid-animation?

level: middleimportance: should knowfreq 35%

answer

  1. animate any value type
  2. builder(context, value, child)
  3. animates begin to end on first build
  4. begin ignored after the first run
  5. takes ownership of the Tween

basics

~20 s

TweenAnimationBuilder animates any value a Tween describes and passes it to a builder, with no dedicated widget needed. It runs begin to end on first build; a later new end animates from the current value.

solid answer

~40 s

`TweenAnimationBuilder<T>` is the build-your-own implicit animation: pass a `tween`, a `duration`, an optional `curve` and a `builder(context, value, child)`, and the builder is called every frame with the interpolated value. Unlike `AnimatedContainer`, it can animate anything a `Tween` can interpolate — a progress arc, a blur radius, a counter — and it **does** animate on first build, from `tween.begin` to `tween.end` (if `begin` is null it starts at `end`). After that, `begin` is ignored: a rebuild with a different `end` re-aims from the value currently on screen and runs a full `duration`. It takes ownership of the `Tween` and mutates it, so create a new `Tween` each build rather than storing one. Put static subtrees in `child` so they are not rebuilt per frame.

code

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

class HelpfulCounter extends StatelessWidget {
  const HelpfulCounter({super.key, required this.votes});

  final int votes;

  @override
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<int>(
      // A new IntTween every build: the builder owns and mutates it.
      tween: IntTween(begin: 0, end: votes),
      duration: const Duration(milliseconds: 600),
      curve: Curves.easeOutCubic,
      builder: (context, value, child) => Row(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[child!, Text(' $value found this helpful')],
      ),
      child: const Icon(Icons.thumb_up_alt_outlined, size: 16),
    );
  }
}

go deeper

for a junior

Recall the parameters, tween, duration, curve, builder and child, and that the builder receives the current value.

for a middle

Explain first-build behaviour, why begin is ignored afterwards, re-aiming from the current value, and the tween-ownership rule.

for a senior

Use child to keep per-frame work small, and know when a dedicated implicit widget or a controller is the better tool.

for a principal

Standardise small custom value animations on TweenAnimationBuilder so teams avoid ad hoc controllers for one-off effects.

## What it is **`TweenAnimationBuilder<T>`** is an `ImplicitlyAnimatedWidget` whose animated property is **a value you choose**. The built-in implicit widgets each animate a fixed set of properties — `AnimatedOpacity` animates opacity, `AnimatedPadding` animates padding. When the thing you want to animate has no dedicated widget, `TweenAnimationBuilder` lets you describe it with a **`Tween`** (an object that interpolates between a `begin` and an `end`) and build whatever widget you like from the current value. Its parameters: - **`tween`** (required) — a `Tween<T>` with a non-null `end`; `begin` may be null. - **`duration`** (required) and **`curve`** (default `Curves.linear`). - **`builder`** (required) — a `ValueWidgetBuilder<T>`: `(BuildContext context, T value, Widget? child)`. - **`child`** — an optional subtree passed through to `builder` unchanged, so it is built once rather than every frame. - **`onEnd`** — called when an animation completes. ## The lifecycle 1. **First build.** If `begin` is null it is set to `end`. If `begin` and `end` differ, the animation runs immediately from `begin` to `end`. This is the one place an implicit widget animates on its first appearance, which makes it useful for entrance effects. 2. **Each frame.** The builder receives the tween's value at the current curve position. 3. **A later rebuild with a different `end`.** The state sets the tween's begin to **the value currently on screen**, its end to the new target, and runs the controller from 0 over the full `duration`. The new tween's `begin` is ignored. 4. **A rebuild with the same `end`.** Nothing restarts; an animation in progress continues. | Situation | Starts from | Goes to | |---|---|---| | first build, `begin` set | `begin` | `end` | | first build, `begin` null | `end` | `end` (no motion) | | new `end` after completion | the old `end` | the new `end` | | new `end` mid-flight | the current value | the new `end` | ## Ownership of the tween The widget **takes ownership of the `Tween` and mutates it** — it rewrites `begin` as it re-aims. The API docs therefore say to create **a new `Tween` instance** whenever the values change, and not to keep one in a field that other code reads or edits. Writing `tween: Tween<double>(begin: 0, end: progress)` inline in `build` is the idiomatic form. ## A help-centre example An article page shows a reading-progress ring that fills as the reader scrolls. `progress` comes from state; the ring eases to each new value: ```dart TweenAnimationBuilder<double>( tween: Tween<double>(begin: 0, end: progress), duration: const Duration(milliseconds: 400), curve: Curves.easeOut, builder: (context, value, child) => Stack( alignment: Alignment.center, children: [ CircularProgressIndicator(value: value), child!, ], ), child: const Icon(Icons.menu_book), ) ``` On first build the ring sweeps from 0 to the current progress; later each new `progress` animates from wherever the ring is. The icon is built once and passed through `child`. ## When to use it — and when not - **Use it** for a single custom value that should ease to a new target on rebuild: a gauge, a colour on a painted shape, a number counting up, a blur or elevation with no implicit wrapper. - **Use a dedicated implicit widget** when one exists (`AnimatedOpacity`, `AnimatedAlign`); it is clearer, and some, like `AnimatedOpacity`, avoid rebuilding the child every frame. - **Use an `AnimationController`** for loops, playback control, gesture-driven progress, or several values on one staggered timeline; `TweenAnimationBuilder` has no `repeat` or `reverse`. - Keep the `builder` light. It runs every frame, so anything that does not depend on `value` belongs in `child`. ## Common mistakes - **Expecting a new `begin` to restart the animation.** It is ignored after the first build. To replay from `begin`, give the `TweenAnimationBuilder` a new key: a new `State` runs its first-build animation again. - **A null `end`.** The builder asserts that `tween.end` is non-null. - **Heavy work in `builder`.** It runs every frame; build static parts once through `child`. - **Reusing one stored `Tween`.** The widget rewrites its `begin`, so the stored object stops meaning what you set.

  • Why does an entrance effect work with TweenAnimationBuilder but not with AnimatedOpacity?
    `TweenAnimationBuilder` animates from `begin` to `end` on its first build. `AnimatedOpacity` starts at whatever opacity it is first built with and only animates when a later rebuild changes it, so an entrance needs a second build that flips the value, for example after the first frame.
  • What goes wrong if you store the Tween in a final field and reuse it?
    The widget mutates the tween's `begin` as it re-aims, so the stored object no longer holds the values you set, and any other code reading it sees changed numbers. The documented rule is to pass a new `Tween` whenever values change and never keep one around.

saying these in an interview costs you the question

  • TweenAnimationBuilder restarts from begin every time end changes.
  • It never animates on its first build, like other implicit widgets.
  • Store the Tween in a field so it is not recreated each build.
  • TweenAnimationBuilder can repeat if you set a long duration.
  • Everything under the builder should be built inside it.