skip to content

In Flutter, how do you write a reusable AnimatedWidget subclass, and when is it preferable to an inline AnimatedBuilder?

level: middleimportance: should knowfreq 32%

answer

  1. a StatefulWidget in disguise
  2. pass the animation as listenable
  3. typed getter, override build
  4. resubscribes in didUpdateWidget
  5. add your own child field

basics

~20 s

Extend AnimatedWidget, pass the animation to super as listenable, expose a typed getter and override build. Its hidden State listens and rebuilds only this widget. Prefer it for a named, reusable animated widget; use AnimatedBuilder for one-off inline effects.

solid answer

~40 s

`AnimatedWidget` is an abstract `StatefulWidget` that takes a `Listenable` named `listenable`. A subclass passes its animation up with `super(listenable: turns)`, exposes it through a typed getter such as `Animation<double> get turns => listenable as Animation<double>`, and overrides `build(BuildContext)`. The framework's private `State` adds a listener in `initState`, moves it to the new listenable in `didUpdateWidget` if the parent passes a different one, removes it in `dispose`, and calls `setState` on each notification while mounted. `AnimatedWidget` has no built-in child slot, so a reusable subclass declares its own `child` field, exactly as `SlideTransition` does. Choose a subclass when the effect is reused or deserves a name and a typed API; choose `AnimatedBuilder` for a one-off combination inside a larger `build`.

code

dart · 26 lines
dart
import 'dart:math' as math;

import 'package:flutter/material.dart';

class SpinningLogo extends AnimatedWidget {
  const SpinningLogo({
    super.key,
    required Animation<double> turns,
    required this.child,
  }) : super(listenable: turns);

  final Widget child;

  Animation<double> get turns => listenable as Animation<double>;

  @override
  Widget build(BuildContext context) {
    return Transform.rotate(
      angle: turns.value * 2 * math.pi,
      child: child,
    );
  }
}

// Usage, where _controller is a repeating AnimationController owned and disposed by the caller:
// SpinningLogo(turns: _controller, child: const Icon(Icons.autorenew, size: 48))

go deeper

for a junior

Recall that the FooTransition widgets are AnimatedWidget subclasses and that a subclass passes its animation to super and overrides build.

for a middle

Walk through the hidden State: listener added in initState, moved in didUpdateWidget, removed in dispose, setState on each tick while mounted.

for a senior

Package repeated animated effects as named AnimatedWidget subclasses with typed parameters and a child field, and keep controller ownership clearly outside them.

for a principal

Decide which animated effects deserve a shared widget in a design-system package and which stay as local builders, balancing reuse against API surface to maintain.

## What AnimatedWidget is **`AnimatedWidget`** is the base class behind Flutter's transition widgets (`SlideTransition`, `ScaleTransition`, `RotationTransition`, `SizeTransition`, `AlignTransition`, `DecoratedBoxTransition`, `PositionedTransition`, `DefaultTextStyleTransition`) and behind `ListenableBuilder` and `AnimatedBuilder`. It is an abstract **`StatefulWidget`** with one required field, `listenable`, and one abstract method, `build(BuildContext context)`. You never see its `State`; the framework supplies a private one. ## What the hidden State does The private state class is short, and knowing it is what the middle-level version of this question checks: 1. **`initState`** - calls `widget.listenable.addListener(...)`. 2. **`didUpdateWidget`** - if the new widget carries a **different** `listenable` than the old one, removes the listener from the old one and adds it to the new one. 3. **`dispose`** - removes the listener. 4. **On each notification** - returns early if no longer `mounted`, otherwise calls `setState`, so the widget's `build` runs again. 5. **`build`** - delegates to your subclass's `build(context)`. So an `AnimatedWidget` is simply the listener-plus-`setState` pattern packaged at the **smallest possible scope**: only this widget rebuilds when the animation ticks. ## Writing a subclass ```dart class SpinningLogo extends AnimatedWidget { const SpinningLogo({super.key, required Animation<double> turns, required this.child}) : super(listenable: turns); final Widget child; Animation<double> get turns => listenable as Animation<double>; @override Widget build(BuildContext context) { return Transform.rotate(angle: turns.value * 2 * math.pi, child: child); } } ``` The conventions, all visible in the framework's own transitions: - take the animation under a **meaningful, typed name** (`turns`, `position`, `sizeFactor`) and forward it as `listenable`; - expose a **typed getter** that casts `listenable` back; - declare a **`child` field** for the part that does not animate - `AnimatedWidget` has none, and without it callers would have to rebuild the subtree themselves; - keep the constructor **`const`** so callers can build it cheaply. To react to several listenables at once, pass `Listenable.merge([a, b])` as the `listenable`. ## AnimatedWidget subclass or AnimatedBuilder | Question | `AnimatedWidget` subclass | `AnimatedBuilder` | |---|---|---| | Reused in several places? | yes - one named widget | copy-pasted builders drift | | Needs a typed, documented API? | constructor parameters | builder closure only | | One-off effect inside a bigger `build`? | overkill | natural fit | | Needs access to the surrounding `State`'s fields? | pass them in | the closure captures them | | Pre-built child support | declare your own `child` | built-in `child` parameter | The framework's own guidance points the same way: `AnimatedBuilder` is for more complex widgets that include an animation as part of a larger build, and for simple cases without additional state, `AnimatedWidget` is the suggested base. ## Mistakes interviewers listen for - Believing the subclass needs its own `State`, `initState` or `dispose` to manage the listener. - Forgetting that `AnimatedWidget` does **not** own the controller: whoever created the `AnimationController` still disposes it. - Omitting a `child` field and constructing the subtree inside `build`, which rebuilds it every tick. - Passing a freshly created `Animation` (for example `controller.drive(...)` inside the parent's `build`) on every rebuild, which makes `didUpdateWidget` move the listener each time. ## How the built-in transitions follow the same recipe Read any `FooTransition` in the framework and the shape repeats. `SlideTransition` takes `position` and forwards it as `listenable`, exposes `Animation<Offset> get position`, declares `child`, and its `build` returns a `FractionalTranslation` around that child. `ScaleTransition` and `RotationTransition` go one step further and share a parent, `MatrixTransition`, which takes an `onTransform` callback turning the value into a `Matrix4`. Writing your own subclass the same way means reviewers recognise it immediately, and it composes with the built-in ones: a `SpinningLogo` can sit inside a `FadeTransition` like any other widget.

  • Who disposes the AnimationController behind an AnimatedWidget subclass?
    Whoever created it - normally the `State` that owns the controller and a ticker provider. `AnimatedWidget` only adds and removes a listener; it never takes ownership of the `Listenable`, so disposing the controller stays in the owner's `dispose` method.
  • How does an AnimatedWidget subclass react to two animations at once?
    Pass `Listenable.merge([first, second])` as the `listenable`. The merged object notifies whenever either source does, so the widget rebuilds once per notification; the subclass reads each value through its own typed fields, since the merged listenable has no value of its own.

saying these in an interview costs you the question

  • An AnimatedWidget subclass must write its own initState and dispose to manage the listener.
  • AnimatedWidget disposes the AnimationController it listens to.
  • AnimatedWidget has a built-in child parameter like AnimatedBuilder.
  • Passing a different animation to an existing AnimatedWidget leaves it listening to the old one.
  • AnimatedWidget is a StatelessWidget that rebuilds automatically.