In Flutter, what does TweenAnimationBuilder add over the built-in implicit widgets, and what happens when its tween's end changes mid-animation?
answer
- animate any value type
- builder(context, value, child)
- animates begin to end on first build
- begin ignored after the first run
- takes ownership of the Tween
basics
~20 sTweenAnimationBuilder 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 linesimport '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
Recall the parameters, tween, duration, curve, builder and child, and that the builder receives the current value.
Explain first-build behaviour, why begin is ignored afterwards, re-aiming from the current value, and the tween-ownership rule.
Use child to keep per-frame work small, and know when a dedicated implicit widget or a controller is the better tool.
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.