Animation
Animation helps people follow changes in your app. It can show where an item went, draw attention to a result, or keep a multi-step action understandable. Fleury provides a few APIs for these different jobs, all driven by the same animation system.
Start with this orbital courier:
Press Launch. The courier moves through ignition, ascent, transfer, and delivery over about five seconds. Press Reset mission at any point to interrupt the launch and return it to the depot.
One animation drives the route, progress bar, color, and status, keeping every part of the mission in sync from launch through delivery.
Choose the right animation tool
Section titled “Choose the right animation tool”| You want to… | Use |
|---|---|
| Move toward a target stored in widget state | AnimationBuilder<T> |
| Own, retarget, await, or chain a value from code | Animation<T> |
| Run visual effects on a widget | Animate |
| Animate a widget in and out of the tree | AnimatedVisibility |
| Advance authored pictures at a chosen interval | FrameBuilder |
| Update a simulation on every app-scheduler tick | Ticker |
Animate toward a target in state
Section titled “Animate toward a target in state”Most motion follows a value your widget already keeps in state: a panel is
open, a package is delivered, a step is complete. Give AnimationBuilder<T>
the target for that state. When the target changes, it animates from its
current value to the new one, and it owns and disposes the animation for you.
This demo sends a package between two destinations. The explicit
AnimationBuilder<double> type means the builder receives a double named
progress, ranging from 0.0 at the depot to 1.0 at the station:
Press the button before the package arrives: the builder retargets from where
the package is, rather than jumping to either end. When the package reaches the
station, the demo mounts a short completion effect. Fleury interpolates
double, int, RgbColor, and CellOffset directly.
Without a curve, AnimationBuilder animates with a spring (Spring.smooth,
or the one you pass as spring:), which keeps its velocity when the target
changes mid-flight. For fixed timing, pass a curve, optionally with a
duration, as this demo does. A duration without a curve throws an
ArgumentError, because a spring sets its own timing.
When part of the builder’s output does not depend on the value, pass it as
child:. The builder receives the same widget on every frame instead of
building it again.
Share one value or time properties separately
Section titled “Share one value or time properties separately”One animated value can drive several visual properties. Use that when size, color, and progress should stay synchronized. Give properties separate builders when they need different durations.
The two status rails below end in the same state. The first uses one
double progress for width and color. In the second, width finishes in 180 ms
while the color continues for 800 ms:
Press Animate and compare the two rails. Shared progress keeps related properties synchronized. Separate builders let each property finish on its own schedule.
Own an animation
Section titled “Own an animation”Own an Animation<T> when code, not a state value, decides the motion: a
sequence of stages, a result you need to await, or one value used in several
places.
This widget owns a double that runs from 0.0 at the depot to 1.0 at the
station. Reading progress.value during build subscribes the widget to the
animation, so it rebuilds as the value changes:
Press Run route. The package moves to the station, waits there for 600 ms, then returns. Press Return now while it is moving or waiting. The orbital courier at the top of this page uses the same primitive for a longer route.
Animation.to follows the same timing rule as AnimationBuilder: a spring by
default, or a curve with an optional duration.
The first .to() starts immediately. Calling .to() or .delay() on the
future it returns appends another stage and returns that same future, so
awaiting it waits for the whole chain. .delay() holds the current value for a
clock-driven duration. A new call on the animation itself replaces the active
chain and starts from the current value:
final run = progress.to(1.0); // starts nowrun.to(0.5); // appends to this runprogress.to(0.0); // replaces and cancels the runThat is why Return now retargets smoothly. The replaced chain’s future
completes with TickerCanceled when you await .orCancel, so _runRoute
returns early instead of resetting _running. Dispose an animation you own with
its owner, as dispose does above.
Apply visual effects
Section titled “Apply visual effects”An Effect describes a visual change such as a fade, slide, flash, or shake.
Animate is the widget that runs one or more effects on its child:
Animate( duration: const Duration(milliseconds: 600), curve: Curves.linear, effects: [Effects.fadeIn(), Effects.slideIn(from: Edge.left)], child: const Text('● Connected to relay'),)The .animate() extension is fluent shorthand for constructing Animate.
This is equivalent to the code above:
const Text('● Connected to relay') .animate( duration: const Duration(milliseconds: 600), curve: Curves.linear, ) .fadeIn() .slideIn(from: Edge.left)Both forms run the same Effect values with Animation<double>; they are not
separate animation systems. This example mounts a connection status and runs
the fade and slide together:
Press Connect to mount the status and run the effects. Animate handles
the visual change; the surrounding if still decides whether the child is in
the tree.
The built-in effects cover entrance and exit motion (fadeIn, fadeOut,
slideIn, slideOut, wipeIn, wipeOut, expand, shrink), feedback
(flash, shake), and looping motion (pulse, shimmer). A looping effect
keeps the animation clock running, and the app drawing frames, for as long as
it is mounted.
Unlike AnimationBuilder, Animate and AnimatedVisibility accept a
duration on its own; their curve defaults to Curves.easeOut.
A slide travels one child width or height by default. Terminal motion lands on
whole cells, so a linear curve and a duration that exposes several cell
positions usually reads best. Pass distance: when you want a shorter,
fixed-cell nudge instead of a full entrance or exit.
Replay feedback from an action
Section titled “Replay feedback from an action”An effect normally runs once when Animate mounts. To replay it without
remounting the widget, set trigger to a value that changes for each action. A
counter works well for repeated submissions. With a trigger, the first build
shows the finished state; the effect plays only when the value changes.
This form validates the pilot name with Form and FormField, as
Forms & validation teaches, so the input shows its
invalid state and carries the error in its semantics. It increments
_submitCount after every submission and shows the outcome in one animated
line, which is why the field sets showErrorMessage: false. Empty input shows
red feedback; a valid pilot name shows green feedback:
Submit the empty field more than once to see the error replay. Then enter a
non-empty name and submit again to see the success state. Both outcomes replay
when _submitCount changes.
Animate visibility changes
Section titled “Animate visibility changes”Animate only runs effects on the child it receives. When an exit must finish
before the child is removed, use AnimatedVisibility. It owns that additional
lifecycle: visible controls whether the child belongs in the tree, while
enter and exit describe each side of the change.
The picker below maps each selection to an Effect, then passes the selected
pair to AnimatedVisibility:
Choose an entrance and exit, then hide and show the sample. Show it again before its exit finishes: the child stays mounted, and the entrance resumes from the exit’s current progress. With a matching pair, such as fade in and fade out or expand and shrink, that looks like a reversal. With the demo’s slide or wipe pair, which enters from the left and leaves to the right, the sample jumps back to the left side.
The picker offers only lifecycle effects; flash and shake are feedback,
while pulse and shimmer loop. Its durations follow distance. The demo’s
sample is 24 columns wide and 9 rows tall, so the horizontal effects run for
800 ms and the size effects for 300 ms. Each then moves about one cell per update of the
animation clock, which keeps every step visible.
Cycle through authored frames
Section titled “Cycle through authored frames”Some motion is a set of distinct pictures rather than values to interpolate:
a typing indicator, cursor blink, progress sprite, or ASCII character cycle.
FrameBuilder rebuilds at the interval you choose and supplies an increasing
integer frame. It still shares Fleury’s app-level scheduler; the widget simply
waits until its interval has elapsed before advancing.
This packet transfer has six authored frames:
Change the speed, then pause and resume it. frame starts at zero and counts
up at the selected interval; modulo maps it onto the six pictures. Resuming or
changing the interval starts frame at zero again. Use Fleury’s
Spinner when its built-in frames are enough, and FrameBuilder when the
frames belong to your app.
Advance a simulation with a ticker
Section titled “Advance a simulation with a ticker”A Ticker runs a callback on Fleury’s animation clock and passes it the total
time elapsed since the ticker started. By default, the clock wakes every 33 ms,
about 30 times a second, while at least one ticker is active, and stops when
the last one stops. This fits game loops, physics, particles, and other motion
that advances continuously rather than toward a target.
The event loop can delay a callback, so compute the simulation from elapsed time rather than counting ticks:
Create the ticker in initState, as this demo does.
SingleTickerProviderStateMixin creates it from the scheduler the current
Fleury app already owns, mutes it with TickerMode, and disposes it with the
state. Application code creates and controls its ticker; it does not create
another scheduler.
The clock’s interval is shared across an app, not set per ticker. A custom host
can provide a TickerScheduler(frameInterval: …) through its TuiBinding. The
separate runApp(frameInterval: …) option limits how often frames are
presented; it does not change how often tickers run.
Next steps
Section titled “Next steps”- Navigation transitions show how motion can preserve context between screens.
- Theming covers the colors and styles animated widgets inherit.
- Neon Asteroids is a complete game loop built on direct ticker control.