Animations & Motion Design

Lottie Animations: Integrating JSON Vector Animations

16 min Lesson 12 of 13

Lottie Animations: Integrating JSON Vector Animations

Lottie is an open-source animation format developed by Airbnb that renders Adobe After Effects animations in real time using JSON-exported vector data. Rather than shipping heavy GIF files or sprite sheets, you load a lightweight .json file that Flutter renders as crisp, scalable vector graphics at any screen density. The result is silky-smooth animations with tiny file sizes and full programmatic control over playback.

Adding the lottie Package

Add the official lottie package to your pubspec.yaml dependencies:

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  lottie: ^3.1.0        # always use the latest stable release

flutter:
  assets:
    - assets/animations/  # folder containing your .json files

Run flutter pub get after saving. Place your .json animation files inside assets/animations/. You can download free animations from LottieFiles.com or export your own from After Effects using the Bodymovin plug-in.

Note: Lottie .json files can also be loaded from the network via URL or from memory as raw bytes — not just from local asset bundles. We will cover the asset approach first as it is the most common in production apps.

Displaying a Lottie Animation

The simplest way to render a Lottie file is with the Lottie.asset() constructor. It behaves like any other Flutter widget and can be placed anywhere in the widget tree:

Basic Lottie Widget

import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';

class SplashScreen extends StatelessWidget {
  const SplashScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Center(
        child: Lottie.asset(
          'assets/animations/rocket_launch.json',
          width: 300,
          height: 300,
          fit: BoxFit.contain,
          // autoplay and loop both default to true
        ),
      ),
    );
  }
}

By default, Lottie.asset() starts playing immediately and loops forever. The width, height, and fit parameters control sizing exactly like an Image widget.

Controlling Playback with an AnimationController

For full control — pausing, reversing, setting speed, or listening for completion — you need a StatefulWidget with an AnimationController. Pass the controller to the controller parameter of the Lottie widget:

AnimationController with Lottie

import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';

class LoadingScreen extends StatefulWidget {
  const LoadingScreen({super.key});

  @override
  State<LoadingScreen> createState() => _LoadingScreenState();
}

class _LoadingScreenState extends State<LoadingScreen>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Lottie.asset(
              'assets/animations/loading_dots.json',
              controller: _controller,
              width: 200,
              height: 200,
              onLoaded: (composition) {
                // Set duration from the JSON metadata, then start looping
                _controller
                  ..duration = composition.duration
                  ..repeat();
              },
            ),
            const SizedBox(height: 24),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => _controller.forward(),
                  child: const Text('Play'),
                ),
                const SizedBox(width: 12),
                ElevatedButton(
                  onPressed: () => _controller.stop(),
                  child: const Text('Pause'),
                ),
                const SizedBox(width: 12),
                ElevatedButton(
                  onPressed: () => _controller.reverse(),
                  child: const Text('Reverse'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
Tip: Always read the animation duration from composition.duration inside the onLoaded callback rather than hardcoding a Duration. Each Lottie JSON file embeds its own intended duration, and setting it from the metadata ensures frame-perfect playback regardless of how the designer exported it.

Adjusting Playback Speed

Speed is controlled via AnimationController.animationBehavior or more commonly by multiplying the controller's clock speed using CurvedAnimation or the speed property on newer Lottie builds. The idiomatic approach uses _controller.duration scaling:

Changing Speed at Runtime

// Half speed: double the duration
_controller.duration = composition.duration * 2;

// Double speed: halve the duration
_controller.duration = composition.duration * 0.5;

// You can expose a speed multiplier via a slider:
double _speed = 1.0;

Slider(
  value: _speed,
  min: 0.25,
  max: 3.0,
  onChanged: (value) {
    setState(() {
      _speed = value;
      _controller.duration = composition.duration * (1 / _speed);
    });
  },
)

Responding to Animation Completion

To run code when a non-looping animation finishes, add a StatusListener to the controller. This is the standard Flutter animation pattern and works identically for Lottie:

Completion Callback

@override
void initState() {
  super.initState();
  _controller = AnimationController(vsync: this);

  // Listen for completion
  _controller.addStatusListener((status) {
    if (status == AnimationStatus.completed) {
      // Animation finished playing forward — navigate or update UI
      Navigator.of(context).pushReplacementNamed('/home');
    }
  });
}

// In onLoaded: play once (no repeat)
onLoaded: (composition) {
  _controller
    ..duration = composition.duration
    ..forward();
}
Warning: Do not call Navigator or setState inside addStatusListener without first checking if (mounted). The status listener can fire after the widget has been disposed, which will throw a FlutterError. Guard all side effects with if (!mounted) return;.

Loading from the Network

You can fetch a Lottie animation from a remote URL using Lottie.network(). This is useful for dynamically themed animations stored on a CDN:

Network Lottie

Lottie.network(
  'https://assets5.lottiefiles.com/packages/lf20_success.json',
  width: 200,
  height: 200,
  errorBuilder: (context, error, stackTrace) {
    return const Icon(Icons.error_outline, size: 64);
  },
)

Summary

Lottie animations integrate into Flutter with minimal boilerplate: add the lottie package, declare your .json assets, and use Lottie.asset() for simple auto-playing animations. For interactive control, pair the widget with an AnimationController, read the duration from onLoaded, and use addStatusListener to respond to completion. Speed adjustments are as simple as scaling the controller duration, making Lottie a powerful yet approachable tool for any Flutter developer.

Key Takeaway: Lottie renders scalable JSON vector animations natively in Flutter. Use Lottie.asset() with default autoplay: true and repeat: true for static decorative animations, and introduce an AnimationController whenever you need play/pause, speed, or completion hooks.