Es bien sabido que Flutter ofrece una forma muy sencilla de crear hermosas aplicaciones multiplataforma (Android e iOS, y Escritorio y Web en el canal beta actualmente), y el uso de animaciones nos viene de perlas para completar el diseño con los temas, modo claro/oscuro en Flutter.
Desde la perspectiva de un desarrollador de Android e IOS (como yo), esto puede ser cierto en muchos niveles, ya que puede crear su interfaz de usuario con código declarativo y funcional.
Utilizando conceptos sencillos como Columna o Fila, en lugar de sumergirse en archivos XML (que no son código como tal), o incluso el famoso storyboards que es a nivel de Interfaz Gráfica; que en conjunto todo esto es un pequeño infierno, aumenta la complejidad de desarrollar aplicaciones para ambas plataformas que sigan una misma línea (por ejemplo, una aplicación de una tienda en línea para un cliente que quiere su app para Android, iOS y por supuesto una web).
Si has trabajado con la API de animación de Android, sabrá que son un dolor de cabeza en muchos niveles: una interfaz compleja, muchas API (algunas de ellas en desuso) y, al final, terminarás sin saber cómo implementar tus animaciones aparte de las librerías de soporte... y sin contar con el proceso de animación en iOS que es un mundo a parte de su competidor más cercano de Android con iOS.
Este no es el caso de las animaciones de Flutter, ¡ya que solo hay una manera de implementarlas! y es común para Android e IOS, ya que recuerda que Flutter no emplea primitivas de iOS por ejemplo, si quieres implementar un botón, no emplea las primitivas de Button en Android y UIButton en iOS, si no lo dibuja TODO desde cero para cada plataforma; lo cual es excelente ya que Flutter tiene control total y gana velocidad.
Por qué Flutter facilita tanto las animaciones
En Android las animaciones implican navegar entre APIs, clases extrañas y comportamientos inconsistentes. En iOS el paradigma es completamente diferente. Recuerdo que mantener ambas versiones sincronizadas en un proyecto multicliente era una locura.
Con Flutter eso desaparece. Las animaciones son idénticas para Android, iOS, Web y escritorio. Todo se dibuja desde cero con el motor de Flutter, por lo que no depende de primitivas nativas (UIButton, View, etc.). Control total, cero sorpresas.
Ventajas reales al animar widgets en Flutter
- Mismo comportamiento en todas las plataformas
- Animaciones fluidas porque el framework controla el render
- APIs limpias y coherentes
- Gran variedad de widgets ya preparados
- Control total cuando quieres ir más allá
Tipos de animaciones en Flutter
- Animaciones implícitas
- Son las más simples. Tú cambias un valor y Flutter hace la transición. Perfectas para empezar.
- Animaciones explícitas
- Aquí controlas la animación en detalle con AnimationController, Tween, Curves y más.
- Interpoladas vs. basadas en física
- Interpoladas: definen inicio y final, y Flutter calcula la transición.
- Basadas en física: usan simulaciones como resortes y gravedad.
- Cuándo elegir cada tipo según tu caso de uso
- Implícitas: microinteracciones, simplicidad, cambios suaves.
- Explícitas: secuencias complejas, animaciones encadenadas, 3D, coordinaciones.
- Físicas: efectos realistas, rebotitos y comportamientos naturales.
Introducción a las animaciones en Flutter
Flutter ofrece un sistema de animaciones muy completo pero que, en su forma más explícita, requiere entender varios componentes que trabajan juntos. Antes de usarlos en nuestra pantalla de autenticación, los practicaremos sobre un ejemplo más simple: una caja de colores.
Los tres elementos fundamentales
Para animar cualquier cosa en Flutter de manera programática necesitamos tres elementos:
- AnimationController: el director de la animación. Controla cuándo empieza, cuándo termina, cuándo se invierte y cuánto dura. Siempre necesita un
vsyncque sincroniza los fotogramas con la pantalla del dispositivo. - Animation: define qué valor se anima y en qué rango. Es la representación del valor que cambia en el tiempo.
- Tween: especifica el punto de inicio (
begin) y el punto de fin (end) de la animación. Permite trabajar con cualquier tipo:double,Offset,Color, etc.
Para que el AnimationController tenga el vsync necesario, la clase del estado debe mezclar el mixin SingleTickerProviderStateMixin:
class _BoxPageState extends State<BoxPage>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _sizeAnimation;
@override
void initState() {
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 2000),
);
_sizeAnimation = Tween<double>(begin: 1.0, end: 6.0).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.linear,
),
);
super.initState();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}El método dispose() es obligatorio. Si no liberamos el controlador cuando el widget se destruye, la animación seguirá consumiendo recursos en segundo plano.
AnimatedBuilder: el widget que conecta todo
Para que los cambios de valor de la animación se reflejen en la pantalla necesitamos reconstruir la parte del árbol de widgets que depende de ese valor. El widget AnimatedBuilder está diseñado exactamente para esto: escucha a una Animation y reconstruye únicamente su subárbol en cada fotograma.
AnimatedBuilder(
animation: _animationController,
child: Center(
child: Container(width: 60, height: 60, color: Colors.blue),
),
builder: (_, Widget? widget) {
return Transform.scale(
scale: _sizeAnimation.value,
child: widget,
);
},
),El parámetro child es el widget que no cambia con la animación. Pasarlo aquí evita que Flutter lo reconstruya en cada fotograma, lo que mejora el rendimiento. El parámetro builder recibe ese mismo child y aplica la transformación animada.
Para iniciar la animación llamamos a forward():
_animationController.forward();Animación infinita: forward y reverse
Una animación que solo va hacia adelante no resulta muy llamativa. Para lograr un efecto continuo de vaivén escuchamos el estado de la animación con un listener y la invertimos automáticamente cuando termina:
_animationController.addListener(() {
if (_animationController.status == AnimationStatus.completed) {
_animationController.reverse();
} else if (_animationController.status == AnimationStatus.dismissed) {
_animationController.forward();
}
});Los cuatro estados posibles de un AnimationController son:
forward: avanzando de inicio a fin.completed: llegó al final.reverse: retrocediendo de fin a inicio.dismissed: volvió al punto de inicio.
También existe el método repeat() que reinicia la animación desde cero al terminar. La diferencia con el ciclo manual es que repeat() siempre arranca desde el valor inicial, mientras que el ciclo con forward/reverse produce un movimiento de ida y vuelta más suave.
Rotación, traslación y opacidad
Una vez entendida la mecánica básica, podemos añadir más animaciones simultáneas. Simplemente declaramos más variables de tipo Animation e inicializamos cada una con su propio Tween.
Para la rotación el valor que animamos es el ángulo en radianes. Multiplicamos por math.pi para expresarlo en términos de vueltas completas:
import 'dart:math' as math;
_rotationAnimation = Tween<double>(begin: 0.0, end: 2.0 * math.pi).animate(
CurvedAnimation(parent: _animationController, curve: Curves.linear),
);Para la traslación usamos un Tween<Offset> porque necesitamos desplazarnos en dos ejes:
_translateAnimate = Tween<Offset>(
begin: Offset.zero,
end: Offset(20.0, 200.0),
).animate(CurvedAnimation(parent: _animationController, curve: Curves.linear));Para la opacidad el rango va de 1.0 (completamente visible) a 0.0 (invisible):
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.5).animate(
CurvedAnimation(parent: _animationController, curve: Curves.linear),
);En el builder del AnimatedBuilder anidamos los widgets de transformación:
builder: (_, Widget? widget) {
return Opacity(
opacity: _opacityAnimation.value,
child: Transform.translate(
offset: _translateAnimate.value,
child: Transform.rotate(
angle: _rotationAnimation.value,
child: Transform.scale(
scale: _sizeAnimation.value,
child: Container(width: 60, height: 60, color: Colors.blue),
),
),
),
);
},El orden de anidamiento importa: la transformación más interna se aplica primero. En este caso primero se escala, luego se rota, luego se traslada y finalmente se aplica la opacidad.
Animando colores con TweenSequence
Para variar el color de fondo a través de múltiples tonos usamos TweenSequence, que encadena varios Tween uno tras otro. Cada elemento de la secuencia tiene un weight (peso) que determina qué proporción del tiempo total ocupa esa transición.
backgroundAnimate = TweenSequence<Color?>([
TweenSequenceItem(
weight: 2.0,
tween: ColorTween(begin: Colors.red, end: Colors.green)),
TweenSequenceItem(
weight: 1.0, tween: ColorTween(begin: Colors.green, end: Colors.blue)),
TweenSequenceItem(
weight: 1.0, tween: ColorTween(begin: Colors.blue, end: Colors.purple))
]);El tipo de esta variable es Animatable<Color>. Para obtener el color en cada fotograma usamos el método evaluate() pasándole el controlador:
color: backgroundAnimate.evaluate(_animationController),Un peso de 2.0 en la primera transición significa que pasará del rojo al verde durante el doble de tiempo que las demás transiciones. Es una forma intuitiva de controlar el ritmo visual de una secuencia de colores.
Encadenando animaciones con Interval
Hasta ahora todas las animaciones ocurren al mismo tiempo. Con la clase Interval podemos indicar en qué momento del ciclo total comienza y termina cada animación individual. Los valores van de 0.0 (inicio del ciclo) a 1.0 (fin del ciclo).
El código de box_page.dart muestra cómo encadenar cuatro animaciones de modo que se ejecuten secuencialmente:
_sizeAnimation = Tween<double>(begin: 1.0, end: 6.0).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(0.0, 0.25, curve: Curves.linear),
),
);
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.5).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(0.25, 0.5, curve: Curves.linear),
),
);
_rotationAnimation = Tween<double>(begin: 0.0, end: 2.0 * math.pi).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(0.3, 0.75, curve: Curves.linear),
),
);
_translateAnimate = Tween<Offset>(begin: Offset.zero, end: Offset(20.0, 200.0)).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(0.75, 1.0, curve: Curves.linear),
),
);Con esta configuración, durante el primer 25% del tiempo la caja se escala, entre el 25% y el 50% varía su opacidad, entre el 30% y el 75% rota, y en el último cuarto se traslada. Los intervalos pueden solaparse, lo que permite que dos animaciones ocurran simultáneamente en ciertos momentos.
AnimatedContainer: animaciones declarativas
El enfoque con AnimationController y AnimatedBuilder es poderoso pero verboso. Para los casos más comunes —cambiar el tamaño, el color, el radio de borde o la sombra de un contenedor— Flutter ofrece una alternativa mucho más simple: AnimatedContainer.
La idea es muy elegante: AnimatedContainer se comporta exactamente igual que un Container normal, con la diferencia de que cuando alguna de sus propiedades cambia de valor, el widget interpola automáticamente entre el valor anterior y el nuevo durante el tiempo especificado en duration. No necesitamos controlador ni Tween; basta con llamar a setState().
class _Box2PageState extends State<Box2Page> {
double _width = 50, _height = 50;
Color _color = Colors.purple;
final BorderRadius _borderRadius = BorderRadius.circular(8);
Offset _offset = Offset(1.5, 2.0);
@override
Widget build(BuildContext context) {
final random = math.Random();
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.update),
onPressed: () {
_width = random.nextInt(200).toDouble();
_height = random.nextInt(200).toDouble();
_color = Color.fromRGBO(
random.nextInt(256),
random.nextInt(256),
random.nextInt(256),
random.nextDouble(),
);
_offset = Offset(
random.nextInt(15).toDouble(),
random.nextInt(15).toDouble(),
);
setState(() {});
},
),
body: Center(
child: AnimatedContainer(
duration: Duration(milliseconds: 500),
decoration: BoxDecoration(
color: _color,
borderRadius: _borderRadius,
boxShadow: [
BoxShadow(
offset: _offset,
color: Colors.black38,
blurRadius: random.nextInt(50).toDouble(),
),
],
),
width: _width,
height: _height,
child: Text('$_width x $_height'),
),
),
);
}
}Cada vez que el usuario pulsa el botón flotante se generan valores aleatorios para el tamaño, el color, el radio de sombra y su desplazamiento. El AnimatedContainer detecta el cambio y anima la transición en 500 milisegundos. El resultado es una caja que cambia de forma, color y sombra de manera completamente fluida.
¿Cuándo usar uno u otro?
AnimatedContainer es la elección correcta cuando únicamente necesitamos animar propiedades visuales de un contenedor y el disparo de la animación es consecuencia directa de un cambio de estado. AnimatedBuilder es necesario cuando queremos control total: animaciones que empiezan por evento externo, encadenamiento preciso con intervalos, transformaciones geométricas complejas como rotación o traslación, o variaciones de color con múltiples pasos.
Animaciones implícitas mediante Widgets
Las animaciones implícitas no son más que widgets que gestionan toda la parte difícil de la animación por ti. En realidad, estos widgets son muy similares a los que quizás ya haya implementado en su código. Estoy hablando de, por ejemplo:
- AnimatedPadding, la contraparte animada del widget Padding.
- AnimatedOpacity, contraparte animada del widget Opacity.
- Entre otras.
Estos dos ejemplos son muy puntuales, y su comportamiento se corresponde mucho con su nombre (¡ESO ES un buen nombre de clase!).
AnimatedOpacity(
opacity: show ? 1 : 0,
duration: Duration(milliseconds: 500),
child: Image.asset('assets/logo.png'),
)Cambias show y tienes una transición suave sin hacer nada más.
Estos widgets solo necesitan algunas cosas para funcionar:
- Un child como casi todo en Flutter, puede ser cualquier cosa (que puede ser cualquier widget),
- Una duración (expresada en horas, minutos, segundos, milisegundos, etc.) y un valor que debe ser administrado por un StatefulWidget. Este último punto es muy importante, ya que cada vez que se cambia el valor dentro de la función setState (por lo tanto, se reconstruye el widget), la animación se lanzará automáticamente mediante una interpolación del valor antiguo y el nuevo que en pocas palabras significa, tener una suave transición de un estado a otro.
import 'package:flutter/material.dart';
class Animation1 extends StatefulWidget {
@override
_Animation1State createState() => _Animation1State();
}
class _Animation1State extends State<Animation1> {
double opacity = 0.0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedOpacity(
child: Image.asset(
'assets/img.png',
fit: BoxFit.fill,
),
duration: Duration(seconds: 3),
opacity: opacity,
),
FlatButton(
onPressed: _changeOpacity,
child: Text('Animate'),
)
],
);
}
void _changeOpacity() {
setState(() {
opacity = 1.0;
});
}
} 
El fragmento de código anterior cambió el valor de la variable opacidad dentro de una función setState, que se tradujo en un desvanecimiento fácil y asombroso en la transición para nuestra imagen.
Y, al final, este widget manejó todas las cosas de la animación por sí mismo. Lo único que teníamos que hacer era cambiar el estado de nuestro widget.
El poder de AnimatedContainer para animar múltiples propiedades
AnimatedContainer permite animar los elementos más comunes como lo es el ancho, alto, color, bordes, padding, radios… todo con solo actualizar el estado.
AnimatedContainer(
height: size,
width: size,
duration: Duration(seconds: 1),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(radius),
),
);Límites de las animaciones implícitas
Cuando empiezas a encadenar widgets o necesitas secuencias, reversas, pausas o controlar intervalos, se vuelve complicado. Lo viví: un infierno de sangrías y widgets anidados. Allí entran las explícitas.
Widget AnimatedContainer
¡Esto es genial, solo puedes usar estos widgets para comenzar a usar animaciones en tu aplicación! Pero hay un problema: las animaciones vistas hasta ahora solo animan una propiedad de nuestro hijo. Eso no es muy flexible y puede causar muchos problemas de escalabilidad si simplemente ajustamos los widgets sin control, lo que obviamente conduce a un código pobre y críptico (sabes que Dart puede ser un infierno de sangría si no se escribe correctamente).
En pocas palabras, estas animaciones las podemos emplear para cosas realmente simples, sin implementar una lógica perse en el momento que queramos arrancar la animación, variar comportamientos, duraciones, revertir o detener la animación, nada de esto y mucho mas NO lo podemos hacer con estos contenedores.
La panacea: AnimatedContainer.
Su comportamiento es exactamente el mismo que el de un contenedor común, pero la mejor parte es que cualquiera de sus propiedades se puede animar. Lo único que tenemos que hacer es decidir qué cambios se le harán a nuestro hijo.
Digamos que queremos cambiar tanto la altura como el ancho de nuestro contenedor, y también su color (sí, los colores se pueden interpolar). Lo primero que debemos hacer es definir los campos de nuestro StatefulWidget:
var imgHeight = 100.0;
var imgWidth = 100.0;
var backgroundColor = Colors.white;Y ahora, nuestro contenedor:
AnimatedContainer(
height: imgHeight,
width: imgWidth,
color: backgroundColor,
child: Image.asset('assets/img.png', fit: BoxFit.fill),
duration: Duration(seconds: 3),
)Y decida cuáles serán los nuevos valores que se asignan en nuestra función setState:
void _changeValues() {
setState(() {
// new values to interpolate
imgWidth = 300;
imgHeight = 300;
backgroundColor = Colors.deepPurpleAccent;
});
} 
Como puede ver, se anima más de una propiedad al mismo tiempo utilizando AnimatedContainer.
Este es el código fuente:
Dije que cualquier propiedad se puede animar en este widget. Probemos y animemos su parámetro de decoración siguiendo los pasos anteriores:
import 'package:flutter/material.dart';
class Animation3 extends StatefulWidget {
@override
_Animation3State createState() => _Animation3State();
}
class _Animation3State extends State<Animation3> {
var imgHeight = 100.0;
var imgWidth = 100.0;
var backgroundColor = Colors.white;
var borderRadius = 0.0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
height: imgHeight,
width: imgWidth,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(borderRadius),
),
child: Image.asset('assets/img.png', fit: BoxFit.fill),
duration: Duration(seconds: 3),
),
FlatButton(
onPressed: _changeValues,
child: Text('Animate'),
)
],
);
}
void _changeValues() {
setState(() {
// new values to interpolate
imgWidth = 300;
imgHeight = 300;
borderRadius = 150.0;
backgroundColor = Colors.deepPurple;
});
}
} 
Como dije, CUALQUIER propiedad del widget Container se puede animar. :) Finalmente, incluso podemos crear una pequeña coreografía secuencial para nuestro pequeño amigo Ditto. Pero esto implicaría rastrear el estado de los campos de la clase, agregando muchas variables nuevas, muchos condicionales para establecer los nuevos valores, y hacerlo nos llevaría a una gran cantidad de repetición.
Otros ejemplos:
Opacidad animada:
AnimatedOpacity(
opacity: visible ? 1 : 0,
duration: Duration(milliseconds: 500),
)Contenedor con varias propiedades animadas:
AnimatedContainer(
duration: Duration(seconds: 1),
width: w,
height: h,
color: color,
)Ejemplo básico con AnimationController
_controller.forward();Flip 3D simple con Transform:
Transform(
transform: Matrix4.identity()..rotateY(pi * value),
alignment: Alignment.center,
child: child,
)Animaciones explícitas: control total paso a paso
A diferencia de las anteriores que solamente definimos un widget algunas propiedades y más, con las explicitas, tenemos el control TOTAL de cuando iniciamos, detenemos, revertimos… (ya entiendes no) las animaciones… PEROOO son más complejas de implementar y requieren un controlador para hacer todo esto.
El papel del AnimationController
Es el “cerebro” de la animación. Define duración, estado, repetición, dirección, pausas y más.
late final AnimationController _controller;
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..forward();Tween, CurvedAnimation y aceleraciones
final curve = CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
);
final animation = Tween<double>(
begin: 0,
end: 300,
).animate(curve);Permiten personalizar cómo avanza la animación.
AnimatedBuilder y AnimatedWidget
Sirven para reconstruir solo la parte afectada en cada frame.
AnimatedBuilder(
animation: animation,
builder: (_, child) {
return Transform.scale(
scale: animation.value,
child: child,
);
},
child: Icon(Icons.star),
)Casos donde realmente necesitas animaciones explícitas
- Secuencias complejas
- Animaciones sincronizadas
- Animaciones 3D (como flip card)
- Juegos y elementos altamente dinámicos
- Control preciso sobre cada frame
Patrones de animación comunes en Flutter
- Hero animations (transiciones entre pantallas)
- El widget Hero permite mover un elemento entre pantallas con continuidad visual.
- Listas y grids animados (AnimatedList)
- Perfectas para añadir y eliminar ítems con suavidad.
- Animaciones escalonadas (Staggered animations)
- Útiles para animar elementos en cascada.
- Transiciones avanzadas con el paquete animations
Preguntas frecuentes
- ¿Qué widget usar para empezar a animar?
- AnimatedContainer o AnimatedOpacity.
- ¿Cómo hacer una animación 3D?
- Con Transform y un AnimationController.
- ¿Son mejores las implícitas o las explícitas?
- Depende: implícitas son rápidas; explícitas te dan control total.
- ¿Cómo evitar que una animación se sienta lenta?
- Ajusta curvas, reduce duración o suaviza cálculos en el build.
Conclusión
Esto es solo para comenzar con las animaciones. Solo quieres que tu aplicación se vea genial sin hacer un gran esfuerzo. La interfaz es simple y fácil de usar.
Después de probar animaciones implícitas, explícitas, contenedores animados y patrones como Hero, verás que Flutter te deja escalar desde lo más simple hasta lo más avanzado sin cambiar de mentalidad. Esa coherencia fue lo que más disfruté cuando pasé de Android/iOS nativos a Flutter.
Si ya dominas esto, te recomiendo explorar:
- Animaciones físicas avanzadas (SpringSimulation)
- TweenSequence para animaciones complejas
- Shared Axis Transitions (paquete animations)
- Flutter Impeller para mejoras de rendimiento
Aprende la arquitectura en Flutter MVVM.