In a Flutter app, when would you play a Lottie or Rive file instead of coding the animation, and how do you control each?
answer
- illustration, not layout
- Lottie is a timeline, Rive a state machine
- animate and repeat default true
- composition.duration in onLoaded
- RiveWidget with RiveWidgetController
basics
~20 sUse files for designer-authored illustration: the lottie package plays After Effects JSON as a timeline you can drive with an AnimationController, while the rive package runs .riv state machines that react to inputs or data binding at runtime.
solid answer
~30 sWhen motion is illustration a designer iterates on, ship a file instead of hand-coded tweens. `Lottie.asset('assets/x.json')` loads an After Effects export and, with no controller, loops because `animate` and `repeat` default to true; for control, pass an `AnimationController`, whose value maps to progress, and set `controller.duration = composition.duration` in `onLoaded` before `forward()`. Rive files hold artboards with state machines: in rive 0.14 and later you call `RiveNative.init()` at startup, load a `File` (for example through `RiveWidgetBuilder` and `FileLoader.fromAsset`), render `RiveWidget(controller: ...)`, and drive the state machine through data binding or inputs. Pick Lottie for timelines, Rive for interactive graphics.
code
dart · 35 linesimport 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
class DeckCompleteBadge extends StatefulWidget {
const DeckCompleteBadge({super.key});
@override
State<DeckCompleteBadge> createState() => _DeckCompleteBadgeState();
}
class _DeckCompleteBadgeState extends State<DeckCompleteBadge>
with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(vsync: this);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Lottie.asset(
'assets/deck_complete.json',
controller: _controller,
width: 160,
onLoaded: (composition) {
// The controller has no duration until the file is parsed.
_controller
..duration = composition.duration
..forward();
},
);
}
}go deeper
Know that Lottie plays After Effects JSON and loops by default, that Rive runs interactive state machines, and that both files are declared assets.
Explain how a controller drives Lottie progress and why duration comes from onLoaded, and how RiveWidget, its controller and data binding fit together.
Judge when a file beats code, watch raster cost and disposal, and handle the rive 0.14 API break when upgrading older integrations.
Set a policy for designer-owned motion assets: which tool, review and size budgets, and how interactive graphics stay testable and accessible.
## Why ship an animation file at all Some motion is illustration rather than layout: a confetti burst when a learner finishes a deck, a mascot that reacts to right and wrong answers. Coding that frame by frame with tweens and `CustomPainter` is slow, and designers cannot iterate on it. Two packages play motion authored in dedicated tools: | | `lottie` | `rive` | |---|---|---| | Source tool | After Effects, exported as JSON (Bodymovin) | the Rive editor | | File | `.json`, or a `.zip` with images | `.riv` binary | | Model | a timeline: progress 0 to 1 | artboards with **state machines** that react at runtime | | Implementation | pure Dart port of lottie-android | `rive_native` C++ runtime and renderer (since rive 0.14) | | Control | an `Animation<double>` mapped to progress | state-machine inputs or data binding | Rule of thumb: **Lottie for fire-and-forget or scrubbed timelines, Rive for interactive, stateful graphics.** Both files are assets, so they must be declared in `pubspec.yaml` like any other. ## Playing a Lottie file - `Lottie.asset(path)` (also `Lottie.network`, `Lottie.file`, `Lottie.memory`) loads, parses and caches the composition. - With no controller it plays by itself: `animate` defaults to true and `repeat` to true, so it **loops**; `reverse` (default false) ping-pongs. - For control, pass your own `AnimationController`. The package maps the controller's value to the animation's **progress**, so `forward()`, `reverse()`, `animateTo(0.5)` or even a spring all work. When a controller is supplied, `animate` and `repeat` are ignored. - The controller has no duration until the file is parsed, which is why the idiom sets `controller.duration = composition.duration` inside `onLoaded` before calling `forward()`; `forward()` on a controller without a duration asserts. - `frameRate` defaults to the composition's own frame rate; `renderCache` (`RenderCache.raster` or `RenderCache.drawingCommands`) trades memory for less work on complex files. ## Playing a Rive file 1. Call `await RiveNative.init()` in `main` after `WidgetsFlutterBinding.ensureInitialized()`; the runtime is native. 2. Load the file, for example with `RiveWidgetBuilder(fileLoader: FileLoader.fromAsset('assets/mascot.riv', riveFactory: Factory.rive), builder: ...)`. The builder receives a sealed `RiveState`: `RiveLoading`, `RiveFailed` or `RiveLoaded`. 3. In the loaded state, render `RiveWidget(controller: state.controller)`. The `RiveWidgetController` runs the chosen artboard and state machine. 4. Drive the state machine from Dart. The package's example recommends **data binding** (view-model properties) over the older **inputs** (`stateMachine.number`, `boolean`, `trigger`). 5. Dispose what you own — a `FileLoader` you create holds the loaded file. `Factory.rive` selects Rive's own renderer; `Factory.flutter` draws through Flutter's renderer. The package README suggests trying both, and comparing with `--no-enable-impeller`, when output differs from the editor. ## Version notes - **rive 0.14** replaced the old pure-Dart runtime with one built on `rive_native`; the last release of the legacy runtime was 0.13.20, so older tutorials use a different widget API. The 0.15 line is in pre-release at the time of writing, adding changes such as automatic view-model binding. - **lottie 3.x** is the current major; recent releases throttle automatic playback to the composition frame rate to cut rebuilds. ## Pitfalls - A Lottie with a custom controller that is never disposed leaks its ticker, like any `AnimationController`. - Expecting a Lottie to respond to taps on its parts: it is a timeline, not a state machine; that is Rive's job. - Rebuilding the loader on every `build`: create the `FileLoader` or composition future once in `State` and reuse it, or the file is fetched and parsed again. - Forgetting accessibility: a decorative animation should be excluded from semantics, and a meaningful one needs a label, whichever package draws it. - Treating either file as free: large compositions still cost raster time on every frame they animate.
- How would you scrub a Lottie animation with a slider?Pass an `AnimationController` as `controller` and set `controller.value` from the slider's `onChanged`, in the range 0 to 1. The lottie package maps the controller's value to the composition's progress, so each value shows the matching frame; `animate` and `repeat` are ignored once a controller is supplied.
- What does the rive package recommend for driving a state machine from Dart?Data binding: properties on a view model that the Rive file declares, bound to the `RiveWidgetController` and set from Dart. The older state-machine inputs (`number`, `boolean`, `trigger`) still work, but the package's own example says it strongly recommends data binding for runtime control.
saying these in an interview costs you the question
- A Lottie file can react to taps on individual shapes like a state machine.
- Lottie.asset plays once and stops unless you pass a controller.
- An AnimationController passed to Lottie needs no duration before forward().
- Rive and Lottie files load without declaring them as assets.
- Rive can only render through Flutter's own renderer.