Lottie en Flutter: ejercicio resuelto con animaciones JSON

Lottie en Flutter: ejercicio resuelto con animaciones JSON

Lottie es el formato estándar para animaciones vectoriales exportadas desde Adobe After Effects. En Flutter, el paquete lottie permite reproducirlas con un solo widget, con control total sobre velocidad, bucles y sincronización con gestos o estado de la app.

Enunciado

Implementa una app que:

  • Reproduce una animación Lottie en bucle con velocidad configurable.
  • Permite pausar y reanudar la animación desde un botón.
  • Muestra una segunda animación Lottie que responde a un slider (progreso manual).
  • Usa AnimationController para sincronizar Lottie con el estado de la app.

Dependencias

1
2
dependencies:
  lottie: ^3.1.2

Descarga archivos .json gratuitos desde LottieFiles y colócalos en assets/animations/.

1
2
3
4
5
# pubspec.yaml
flutter:
  assets:
    - assets/animations/loading.json
    - assets/animations/success.json

Solución completa

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';

void main() => runApp(const MaterialApp(home: LottieDemo()));

class LottieDemo extends StatefulWidget {
  const LottieDemo({super.key});
  @override
  State<LottieDemo> createState() => _LottieDemoState();
}

class _LottieDemoState extends State<LottieDemo>
    with TickerProviderStateMixin {
  // Controlador para la animación en bucle
  late final AnimationController _loopController;

  // Controlador para la animación manual (slider)
  late final AnimationController _manualController;

  double _speed = 1.0;
  double _manualProgress = 0.0;
  bool _isPlaying = true;

  @override
  void initState() {
    super.initState();

    _loopController = AnimationController(vsync: this);
    _manualController = AnimationController(vsync: this);
  }

  @override
  void dispose() {
    _loopController.dispose();
    _manualController.dispose();
    super.dispose();
  }

  void _togglePlayPause() {
    setState(() => _isPlaying = !_isPlaying);
    if (_isPlaying) {
      _loopController.forward();
    } else {
      _loopController.stop();
    }
  }

  void _onSpeedChanged(double value) {
    setState(() => _speed = value);
    _loopController.duration = _loopController.duration! ~/
        Duration(microseconds: (_speed * 1000000).round()) *
        const Duration(microseconds: 1000000);
    // Forma más sencilla: simplemente recrear con repeat
  }

  void _onManualProgressChanged(double value) {
    setState(() => _manualProgress = value);
    _manualController.value = value;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Lottie en Flutter')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // ── Animación en bucle ──────────────────────────────────────────
            const Text('Animación en bucle',
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
            const SizedBox(height: 8),
            Lottie.asset(
              'assets/animations/loading.json',
              controller: _loopController,
              height: 180,
              onLoaded: (composition) {
                // Asignar duración y arrancar en bucle
                _loopController
                  ..duration = composition.duration
                  ..repeat();
              },
            ),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                FilledButton.icon(
                  onPressed: _togglePlayPause,
                  icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow),
                  label: Text(_isPlaying ? 'Pausar' : 'Reanudar'),
                ),
                const SizedBox(width: 12),
                // Control de velocidad
                const Text('Velocidad:'),
                SizedBox(
                  width: 120,
                  child: Slider(
                    min: 0.25,
                    max: 3.0,
                    divisions: 11,
                    label: '${_speed.toStringAsFixed(2)}x',
                    value: _speed,
                    onChanged: (v) {
                      setState(() => _speed = v);
                      // Ajustar velocidad cambiando la duración
                      final baseDuration = const Duration(milliseconds: 2000);
                      _loopController.duration =
                          baseDuration ~/ v.round().clamp(1, 10);
                      if (_isPlaying) _loopController.repeat();
                    },
                  ),
                ),
                Text('${_speed.toStringAsFixed(1)}x'),
              ],
            ),

            const Divider(height: 40),

            // ── Animación controlada manualmente ───────────────────────────
            const Text('Control manual de frame',
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
            const SizedBox(height: 8),
            Lottie.asset(
              'assets/animations/success.json',
              controller: _manualController,
              height: 180,
              onLoaded: (composition) {
                _manualController.duration = composition.duration;
              },
            ),
            Row(
              children: [
                const Text('Progreso:'),
                Expanded(
                  child: Slider(
                    min: 0.0,
                    max: 1.0,
                    value: _manualProgress,
                    onChanged: _onManualProgressChanged,
                  ),
                ),
                Text('${(_manualProgress * 100).round()}%'),
              ],
            ),

            const Divider(height: 40),

            // ── Lottie desde red (URL) ──────────────────────────────────────
            const Text('Lottie desde URL',
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
            const SizedBox(height: 8),
            Lottie.network(
              'https://assets5.lottiefiles.com/packages/lf20_fcfjwiyb.json',
              height: 150,
              repeat: true,
              errorBuilder: (_, error, __) =>
                  const Text('No se pudo cargar la animación de red'),
            ),

            const Divider(height: 40),

            // ── Animación que responde a gestos ─────────────────────────────
            const Text('Respuesta a tap',
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
            const SizedBox(height: 8),
            _TapLottie(),
          ],
        ),
      ),
    );
  }
}

// ── Widget de Lottie con respuesta a tap ───────────────────────────────────────
class _TapLottie extends StatefulWidget {
  @override
  State<_TapLottie> createState() => _TapLottieState();
}

class _TapLottieState extends State<_TapLottie> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  bool _triggered = false;

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

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

  void _onTap() {
    if (_triggered) {
      _controller.reset();
    } else {
      _controller.forward();
    }
    setState(() => _triggered = !_triggered);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _onTap,
      child: Column(
        children: [
          Lottie.asset(
            'assets/animations/success.json',
            controller: _controller,
            height: 150,
            onLoaded: (c) => _controller.duration = c.duration,
          ),
          Text(
            _triggered ? 'Toca para reiniciar' : 'Toca para animar',
            style: const TextStyle(color: Colors.grey),
          ),
        ],
      ),
    );
  }
}

Cómo funciona AnimationController con Lottie

ModoCódigoResultado
Bucle automático_controller.repeat()Animación continua
Una vez_controller.forward()Reproduce y detiene
Inversa_controller.reverse()Reproduce al revés
Frame manual_controller.value = 0.5Salta al 50% de la animación
VelocidadCambiar _controller.durationMás corta = más rápida

Errores frecuentes

  • Pantalla negra al cargar Lottie: el asset no está declarado en pubspec.yaml o la ruta es incorrecta. Verifica flutter pub get y la ruta exacta.
  • onLoaded nunca se llama: es normal si usas Lottie.network y no hay conexión. Usa errorBuilder para manejarlo.
  • Animación va muy rápida o muy lenta: debes asignar _controller.duration = composition.duration dentro de onLoaded. Si no lo haces, el controlador usará su duración por defecto (1 segundo).

Aplicación práctica

Lottie se usa para: pantallas de carga (loading spinners complejos), estados vacíos animados, celebraciones tras completar una acción, onboarding animado y botones con micro-animaciones.

Siguiente ejercicio recomendado

Práctica guiada y siguiente paso

FAQ

¿Puedo usar archivos Lottie de Lottie Web en Flutter?

Sí. El formato JSON de Lottie es compatible entre plataformas. Descarga el archivo .json desde LottieFiles y úsalo como asset local o con Lottie.network.

¿Lottie afecta el rendimiento?

Las animaciones Lottie simples tienen impacto mínimo. Para animaciones complejas con muchas capas, usa RenderingStrategy.canvas o considera simplificar el archivo en After Effects. Usa Flutter DevTools para verificar el frame rate.

¿Puedo cambiar colores de la animación Lottie desde Flutter?

El paquete soporta delegates para LottieDelegates que permiten sobrescribir colores de capas específicas. Úsalo cuando necesites theming dinámico sin modificar el archivo JSON.