Flutter StatelessWidget vs StatefulWidget: comprensión profunda
Entender la diferencia entre StatelessWidget y StatefulWidget es la base del desarrollo con Flutter.
Esta sección analizará en profundidad los escenarios de uso y el mecanismo de funcionamiento interno de estos dos tipos de Widget.
Diferencia central
En Flutter hay dos tipos principales de Widgets; su diferencia fundamental radica en si pueden mantener un estado mutable.
| Característica | StatelessWidget | StatefulWidget |
|---|---|---|
| Estado | Inmutable | Mutable |
| Momento de reconstrucción | Solo cuando el Widget padre se reconstruye | Cuando el estado cambia + cuando el Widget padre se reconstruye |
| Rendimiento | Mayor (sin necesidad de escuchar el estado) | Ligeramente menor (requiere gestión de estado) |
| Escenarios de uso | Contenido estático | Contenido dinámico interactivo |
Explicación detallada de StatelessWidget
StatelessWidget es adecuado para contenido que no cambia con el tiempo. Una vez creado, todas sus propiedades son final y no se pueden modificar.
Escenarios de uso
- Mostrar texto, imágenes e iconos estáticos
- Mostrar listas de datos (la fuente de datos proviene del Widget padre)
- Contenedores de diseño que no incluyen interacción del usuario
- La porción de solo lectura en formularios
Ejemplo: ejemplo completo de StatelessWidget
// Componente de visualización de información de usuario (sin estado)
class UserCard extends StatelessWidget {
// Las propiedades se definen como final para garantizar la inmutabilidad
final String name;
final String email;
final String avatarUrl;
// El constructor const mejora el rendimiento
const UserCard({
super.key,
required this.name,
required this.email,
this.avatarUrl = '',
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
// Avatar
CircleAvatar(
backgroundImage: avatarUrl.isNotEmpty
? NetworkImage(avatarUrl)
: null,
child: avatarUrl.isEmpty
? const Icon(Icons.person)
: null,
),
const SizedBox(width: 16),
// Información del usuario
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
email,
style: TextStyle(
color: Colors.grey[600],
),
),
],
),
),
],
),
),
);
}
}
Explicación detallada de StatefulWidget
StatefulWidget es adecuado para escenarios que necesitan responder a interacciones del usuario o cambios de datos. Se compone de dos partes: el propio Widget (inmutable) y el objeto State (mutable).
Escenarios de uso
- Datos dinámicos como contadores, temporizadores, etc.
- Entrada de formularios
- Control de animaciones
- Visualización del estado de solicitudes de red
- Respuesta a la interacción del usuario
Ciclo de vida del objeto State
| Método | Descripción |
|---|---|
| initState | Se llama cuando el Widget se crea por primera vez, para inicializar el estado. |
| didChangeDependencies | Se llama cuando cambian las dependencias del Widget. |
| build | Construye la UI; se llama cada vez que cambia el estado. |
| didUpdateWidget | Se llama cuando el Widget padre se reconstruye. |
| dispose | Se llama cuando el Widget se elimina, para limpiar recursos. |
Ejemplo: ejemplo completo de StatefulWidget
// Aplicación de contador
class CounterApp extends StatefulWidget {
const CounterApp({super.key});
@override
State<CounterApp> createState() => _CounterAppState();
}
class _CounterAppState extends State<CounterApp> {
// Estado del contador
int _counter = 0;
// Temporizador (para demostrar dispose)
Timer? _timer;
// 1. initState - inicializar el estado
@override
void initState() {
super.initState();
// Inicializar el contador
_counter = 0;
// Iniciar un temporizador (incrementa una vez por segundo)
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_increment();
});
}
// 2. didChangeDependencies - se llama cuando cambian las dependencias
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
// 3. build - construir la UI
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Contador'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Conteo actual:',
style: TextStyle(fontSize: 20),
),
Text(
'$_counter',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
],
),
),
// Botón de incremento
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
),
);
}
// Método para incrementar el contador
void _increment() {
// Se debe llamar a setState para desencadenar la actualización de la UI
setState(() {
_counter++;
});
}
// 4. didUpdateWidget - se llama cuando el Widget padre se reconstruye
@override
void didUpdateWidget(CounterApp oldWidget) {
super.didUpdateWidget(oldWidget);
}
// 5. dispose - limpiar recursos
@override
void dispose() {
// Cancelar el temporizador para evitar fugas de memoria
_timer?.cancel();
super.dispose();
}
}
Uso correcto de setState
En StatefulWidget, es necesario notificar a Flutter que el estado ha cambiado mediante setState.
Uso correcto
Ejemplo: uso correcto de setState
void _increment() {
setState(() {
_counter++;
});
}
// Correcto: si la actualización del estado no está relacionada con la UI, no es necesario usar setState
void _someInternalLogic() {
// Lógica interna, no afecta a la UI
_calculateSomething();
}
// Correcto: actualizar el estado después de una operación asíncrona
Future<void> _loadData() async {
final data = await fetchData();
setState(() {
_items = data;
});
}
Uso incorrecto
Ejemplo: uso incorrecto de setState
void _increment() {
_counter++; // El estado ha cambiado, ¡pero la UI no se actualizará!
}
// Incorrecto: llamar a setState dentro del método build
@override
Widget build(BuildContext context) {
// ¡Nunca llames a setState en build!
setState(() {
_counter++; // Esto causará un bucle infinito
});
return Text('$_counter');
}
setState hará que se vuelva a llamar al método build en el siguiente fotograma.
Llamar a setState con frecuencia o hacerlo dentro del método build puede provocar problemas de rendimiento.
Cómo elegir
Al desarrollar, se debe elegir el tipo de Widget adecuado según las necesidades reales:
Elija StatelessWidget cuando:
- La apariencia del Widget no depende de ningún estado mutable
- Todos los datos necesarios se pasan desde el Widget padre mediante el constructor
- El Widget no necesita responder a las interacciones del usuario
- Se busca el mejor rendimiento
Elija StatefulWidget cuando:
- El Widget necesita mantener un estado interno
- Se necesita responder a la entrada del usuario (clics, deslizamientos, etc.)
- Se necesita responder a cambios de datos (como el retorno de una solicitud de red)
- Se necesita controlar animaciones