Cómo agregar animación a una app de Flutter
Por el equipo de Charmotion · Actualizado el

Para agregar animación a una app de Flutter, usa widgets implícitos como AnimatedContainer para cambios simples en la UI y un AnimationController cuando necesites controlar los tiempos. Usa Hero para transiciones de elementos compartidos, el paquete lottie para loops vectoriales hechos por diseñadores, el paquete rive para personajes interactivos y video_player para clips.
Puntos clave
- Las animaciones implícitas animan por ti el paso del valor anterior al nuevo; las explícitas se ejecutan con un
AnimationController. Heroimplementa transiciones de elementos compartidos entre rutas.- El paquete
lottie(3.6.x) está escrito íntegramente en Dart y reproduce archivos conLottie.asset. - El paquete
rive(0.14.x) necesitaRiveNative.init(), y luegoFile.assety unRiveWidgetController. - Revisa
MediaQuery.disableAnimationsOf(context)para respetar la configuración de movimiento del usuario.
¿Ya tienes un personaje o logo? Mira tu personaje en pantallas reales de una app: pantallas de splash, onboarding, carga y estado vacío. Es gratis, funciona en tu navegador y no sube nada.
¿Qué enfoque de animación conviene usar en Flutter?
Elige la herramienta según lo que se mueve y de dónde viene el arte:
| Quieres animar | Uso |
|---|---|
| Tamaño, color, padding u opacidad tras un cambio de estado | Widgets implícitos: AnimatedContainer, AnimatedOpacity, TweenAnimationBuilder |
| Loops, movimiento escalonado o controlado por gestos | Un AnimationController con widgets de transición |
| Un elemento que se mueve entre dos pantallas | Hero |
| Un loop vectorial hecho por un diseñador | El paquete lottie |
| Un personaje que reacciona a eventos de la app | El paquete rive con una máquina de estados |
| Un clip fijo y prerrenderizado | El plugin video_player |
La animación integrada no necesita assets ni paquetes adicionales, así que empieza por ahí para el feedback de la UI. Agrega un paquete solo cuando un diseñador te entregue un archivo. Lottie y Rive funcionan en iOS, Android, escritorio y web, así que un mismo asset sirve para todas las plataformas de tu app de Flutter.
Una regla sencilla para elegir paquete: si la animación se reproduce de principio a fin, basta con Lottie o video. Si tu código tiene que indicarle qué hacer a continuación, como "sigue en loop mientras se sube el archivo y luego celebra", usa Rive.
¿Cuál es la diferencia entre animaciones implícitas y explícitas?
La documentación de Flutter divide la animación integrada en dos tipos:
- Las animaciones implícitas animan un cambio por ti. Dale a
AnimatedContainer,AnimatedOpacityoTweenAnimationBuilderun valor nuevo y una duración, y Flutter interpola desde el valor anterior. - Las animaciones explícitas las controla tu propio
AnimationController. Tú decides cuándo iniciar, detener, repetir o invertir.
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
width: selected ? 200 : 120,
height: 120,
decoration: BoxDecoration(
color: selected ? Colors.indigo : Colors.grey,
borderRadius: BorderRadius.circular(16),
),
)
Llama a setState para cambiar selected, y el contenedor anima su ancho y su color. Empieza con widgets implícitos: cubren la mayoría de los botones, tarjetas e interruptores.
Si ningún widget implícito predefinido se ajusta a lo que necesitas, TweenAnimationBuilder anima cualquier valor que puedas describir con un Tween, como un ángulo de rotación, y reconstruye su builder con el valor actual en cada fotograma.

¿Cómo se usan AnimationController y Hero?
Un AnimationController produce valores de 0.0 a 1.0 durante una duración determinada y necesita un ticker vsync, normalmente de SingleTickerProviderStateMixin:
class Pulse extends StatefulWidget {
const Pulse({super.key, required this.child});
final Widget child;
@override
State<Pulse> createState() => _PulseState();
}
class _PulseState extends State<Pulse> with SingleTickerProviderStateMixin {
late final AnimationController controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
)..repeat(reverse: true);
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) =>
FadeTransition(opacity: controller, child: widget.child);
}
Libera siempre el controller con dispose. Los widgets de transición como FadeTransition, ScaleTransition y SlideTransition reconstruyen solo lo que animan.
Para una transición de elemento compartido entre rutas, envuelve el mismo widget en ambas pantallas con Hero y una etiqueta coincidente:
Hero(tag: 'avatar', child: CircleAvatar(backgroundImage: image))
Cuando haces push de la segunda ruta, Flutter desplaza el widget de una posición a la otra.
¿Cómo se agrega un archivo Lottie en Flutter?
Agrega el paquete lottie (3.6.1 al momento de escribir esto). Está escrito en Dart puro, así que funciona en cualquier plataforma donde funcione Flutter. Declara el archivo como asset:
dependencies:
lottie: ^3.6.1
flutter:
assets:
- assets/loading.json
Luego reprodúcelo con Lottie.asset, o con Lottie.network si el archivo está alojado en un servidor:
import 'package:lottie/lottie.dart';
Lottie.asset('assets/loading.json', width: 160, height: 160)
Por defecto, la animación se reproduce y se repite. Pasa tu propio AnimationController mediante el parámetro controller si necesitas iniciarla con un evento o sincronizarla con otra cosa. El paquete también admite archivos dotLottie (.lottie) mediante un loader personalizado.
Lottie funciona bien para spinners, checks de éxito e ilustraciones de onboarding que se reproducen de principio a fin. Es menos adecuado para un personaje cuyo comportamiento debe cambiar a mitad del loop según el estado de la app.
¿Cómo agregar un archivo de Rive y cambiar de estado en Flutter?
Agrega el paquete rive. La documentación de Rive indica rive: ^0.14.11, que depende de rive_native y te permite elegir el renderizador de Rive (Factory.rive) o el de Flutter (Factory.flutter). Declara assets/character.riv en pubspec.yaml y luego inicializa el runtime una sola vez:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await RiveNative.init();
runApp(const MyApp());
}
Carga el archivo, crea un controller para la máquina de estados (state machine) y renderízalo con RiveWidget:
final f = await File.asset('assets/character.riv', riveFactory: Factory.rive);
final controller = RiveWidgetController(
f!,
stateMachineSelector: StateMachineSelector.byName('CharacterStateMachine'),
);
// in build():
RiveWidget(controller: controller, fit: Fit.contain)
Para cambiar de estado en un archivo basado en inputs, busca los inputs en la máquina de estados y asígnales un valor:
final mood = controller.stateMachine.number('mood');
final success = controller.stateMachine.trigger('success');
mood?.value = 5; // loading loop
success?.fire(); // one-shot reaction
Haz la carga en initState, no en build, y libera el controller, los inputs y el archivo cuando el widget desaparezca. Los inputs están obsoletos en favor de Data Binding, que el runtime de Rive para Flutter también admite en archivos con view models. Consulta la guía de Rive en Flutter para ver un widget completo.
¿Cómo reproducir un video en loop con video_player?
Para un clip fijo, el plugin video_player es la opción estándar. Crea un controller, inicialízalo y luego activa el loop y la reproducción:
late final VideoPlayerController video;
@override
void initState() {
super.initState();
video = VideoPlayerController.asset('assets/logo_reveal.mp4')
..setLooping(true)
..setVolume(0)
..initialize().then((_) {
setState(() {});
video.play();
});
}
@override
void dispose() {
video.dispose();
super.dispose();
}
// in build():
video.value.isInitialized
? AspectRatio(aspectRatio: video.value.aspectRatio, child: VideoPlayer(video))
: const SizedBox.shrink()
La cascada (..) configura el controller antes de que initialize() termine; el callback then reconstruye el widget cuando el primer fotograma está disponible. Silenciar con setVolume(0) mantiene sin sonido un loop decorativo.
El video maneja bien el detalle fotográfico y 3D, pero cada variación es otro archivo y nada reacciona al estado. Úsalo para una animación de presentación al abrir la app o un loop de marketing, y un archivo de Rive para un personaje que tenga que responder al usuario.
¿Cuál es la mejor opción para una mascota de marca animada en Flutter?
Para una mascota de marca que reacciona a la app, Rive es la opción práctica: un solo archivo contiene todos los loops y reacciones, y una máquina de estados gestiona las transiciones. Tu código asigna los eventos de la app a valores:
- Pantalla en reposo: mood 0. Llamada de red en curso: mood 5.
- Llamada terminada: dispara
successoerror. - Usuario ausente un rato: un loop de sueño.
Mantén esta asignación en un solo lugar, como un listener en tu gestión de estado (un ChangeNotifier, Bloc o provider de Riverpod) que actualice los inputs. Así los widgets nunca necesitan saber qué animación se está reproduciendo.

Lottie sirve para una mascota que solo reproduce un loop por pantalla; es posible cambiar de archivo para cada estado, pero las transiciones se cortarán. El video es la opción más pesada y la menos flexible.
Si tienes un personaje pero no un animador, Charmotion genera un set de 17 animaciones a partir de una carga, incluido un .riv cuya CharacterStateMachine recibe un número mood de 0 a 9 y triggers como success y error, así que las llamadas de Flutter anteriores funcionan sin cambios. Consulta estados de animación de una mascota para saber qué estados planificar.
¿Cómo mantener las animaciones de Flutter fluidas y accesibles?
La mayoría de los tirones vienen de reconstruir demasiado en cada fotograma. Algunos hábitos ayudan:
- Usa widgets de transición (
FadeTransition,ScaleTransition) oAnimatedBuildercon unchild, para que solo se reconstruya la parte animada. - Libera (dispose) cada
AnimationController, controller de Rive y controller de video. - Carga los archivos Lottie y Rive una vez por pantalla, nunca dentro de
build. - Mide el rendimiento en modo profile en un dispositivo real; el modo debug es mucho más lento.
Respeta el ajuste de movimiento reducido de la plataforma. MediaQuery.disableAnimationsOf(context) devuelve el valor de MediaQueryData.disableAnimations más cercano y solo reconstruye cuando esa propiedad cambia:
final reduce = MediaQuery.disableAnimationsOf(context);
return reduce
? Image.asset('assets/mascot_still.png', width: 220)
: RiveWidget(controller: controller, fit: Fit.contain);
Mantén disponible en texto la información que transmite la animación, como "cargando". La guía de movimiento reducido explica las reglas de WCAG.
Agrega animación a una app de Flutter
- Empieza con implícitas. Usa
AnimatedContainer,AnimatedOpacityoTweenAnimationBuilderpara cambios simples de UI. - Pasa a explícitas si hace falta. Crea un
AnimationControllerconvsyncpara loops y secuencias temporizadas, y libéralo con dispose. - Agrega paquetes para el arte. Agrega
lottieoriveapubspec.yamly declara los archivos enflutter: assets:. - Renderiza el archivo. Usa
Lottie.asset(), o llama aRiveNative.init()y renderizaRiveWidgetcon unRiveWidgetController. - Conecta el estado de la app. Asigna
stateMachine.number('mood').valuey dispara triggers cuando cambie tu estado. - Respeta los ajustes de movimiento. Comprueba
MediaQuery.disableAnimationsOf(context)y muestra una imagen fija cuando sea true.
Preguntas frecuentes
¿Debo usar Lottie o Rive en Flutter?
Usa Lottie para animaciones vectoriales lineales creadas en After Effects, como spinners y checks de éxito. Usa Rive cuando la animación deba reaccionar al estado de la app, como una mascota que cambia entre reposo (idle), carga y éxito, porque su máquina de estados gestiona las transiciones dentro de un solo archivo. Ambos paquetes funcionan en móvil, escritorio y web.
¿Por qué mi AnimationController necesita vsync?
El ticker vsync le indica al controller cuándo toca un nuevo fotograma y lo pausa cuando el widget no está visible, lo que ahorra batería. Agrega SingleTickerProviderStateMixin a tu clase State y pasa this como vsync. Usa TickerProviderStateMixin si un mismo State tiene varios controllers.
¿RiveNative.init() tiene que ejecutarse antes que todo lo demás?
Debe ejecutarse antes de cargar cualquier archivo de Rive. Los ejemplos de Rive llaman a WidgetsFlutterBinding.ensureInitialized() y luego hacen await RiveNative.init() en main(), antes de runApp. Deja la llamada en el arranque y no dentro de un widget, para que se ejecute exactamente una vez y cada pantalla pueda cargar archivos de inmediato.
Respuestas rápidas de esta guía
Fuentes
- Introduction to animations, Flutter
- lottie package, pub.dev
- rive package, pub.dev
- Flutter runtime, Rive
- video_player package, pub.dev
- MediaQuery.disableAnimationsOf, Flutter API