skip to content

In a Flutter app, when would you play a Lottie or Rive file instead of coding the animation, and how do you control each?

level: juniorimportance: should knowfreq 42%

answer

  1. illustration, not layout
  2. Lottie is a timeline, Rive a state machine
  3. animate and repeat default true
  4. composition.duration in onLoaded
  5. RiveWidget with RiveWidgetController

basics

~20 s

Use 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 s

When 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 lines
dart
import '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

for a junior

Know that Lottie plays After Effects JSON and loops by default, that Rive runs interactive state machines, and that both files are declared assets.

for a middle

Explain how a controller drives Lottie progress and why duration comes from onLoaded, and how RiveWidget, its controller and data binding fit together.

for a senior

Judge when a file beats code, watch raster cost and disposal, and handle the rive 0.14 API break when upgrading older integrations.

for a principal

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.