Los widgets avanzados te permiten crear interfaces sofisticadas con scroll personalizado, animaciones fluidas y elementos visuales complejos. Perfectos para apps profesionales.
Los Slivers son widgets especializados que trabajan dentro de un CustomScrollView para crear efectos de scroll personalizados y complejos.
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200.0,
floating: false,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text('Mi App'),
background: Image.network(
'https://picsum.photos/400/200',
fit: BoxFit.cover,
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
leading: Icon(Icons.star),
title: Text('Item $index'),
subtitle: Text('Descripción del item $index'),
);
},
childCount: 20,
),
),
],
) El SliverAppBar es una barra de título que se puede expandir y colapsar. Es perfecta para apps con contenido extenso.
SliverAppBar(
expandedHeight: 250.0,
floating: true, // Se muestra al hacer scroll hacia arriba
pinned: true, // Permanece visible cuando se colapsa
snap: true, // Se expande/colapsa completamente
// Contenido cuando está expandido
flexibleSpace: FlexibleSpaceBar(
title: Text(
'Título Dinámico',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
background: Stack(
fit: StackFit.expand,
children: [
Image.network(
'https://picsum.photos/400/250',
fit: BoxFit.cover,
),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.7),
],
),
),
),
],
),
centerTitle: true,
titlePadding: EdgeInsets.only(left: 16, bottom: 16),
),
// Acciones en la barra
actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.more_vert),
onPressed: () {},
),
],
// Color de fondo cuando está colapsado
backgroundColor: Colors.blue,
// Elevación
elevation: 4.0,
) El SliverGrid permite crear grillas de elementos con un número fijo de columnas. Es perfecto para mostrar listas grandes de elementos.
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 1.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
decoration: BoxDecoration(
color: Colors.blue[(index % 9) * 100] ?? Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.star,
color: Colors.white,
size: 40,
),
SizedBox(height: 8),
Text(
'Item $index',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
childCount: 20,
),
) El widget GridView.count es una forma sencilla de crear grillas con un número fijo de columnas. Es perfecto para mostrar listas pequeñas de elementos.
GridView.count(
crossAxisCount: 2,
padding: EdgeInsets.all(16),
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: List.generate(20, (index) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.purple],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.3),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.apps,
color: Colors.white,
size: 40,
),
SizedBox(height: 8),
Text(
'App ${index + 1}',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
);
}),
) El GridView.builder es una forma eficiente de crear grillas cuando tienes una cantidad grande de elementos. Es perfecto para mostrar listas infinitas.
class GrillaEficiente extends StatelessWidget {
final List<Map<String, dynamic>> items = List.generate(100, (index) => {
'title': 'Producto ${index + 1}',
'price': '$${(index + 1) * 10}',
'color': Colors.primaries[index % Colors.primaries.length],
});
Widget build(BuildContext context) {
return GridView.builder(
padding: EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.8,
),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 3,
child: Container(
decoration: BoxDecoration(
color: item['color'],
borderRadius: BorderRadius.vertical(
top: Radius.circular(12),
),
),
child: Icon(
Icons.shopping_bag,
color: Colors.white,
size: 50,
),
),
),
Expanded(
flex: 2,
child: Padding(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['title'],
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4),
Text(
item['price'],
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
),
),
],
),
);
},
);
}
} El widget ClipRRect permite redondear los bordes de cualquier widget hijo. Es muy útil para crear interfaces con esquinas redondeadas.
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.network(
'https://picsum.photos/200/200',
width: 200,
height: 200,
fit: BoxFit.cover,
),
) El ClipRRect también permite redondear los bordes de manera personalizada. Puedes especificar diferentes radios para cada esquina.
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10),
),
child: Container(
width: 250,
height: 150,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.orange, Colors.red],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Center(
child: Text(
'Bordes Personalizados',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
),
) El widget CircleAvatar es una forma sencilla de mostrar avatares circulares. Puedes usarlo para mostrar imágenes de perfil, iniciales o íconos.
// Avatar con imagen de red
CircleAvatar(
radius: 50,
backgroundImage: NetworkImage('https://picsum.photos/100/100'),
backgroundColor: Colors.grey[300],
onBackgroundImageError: (exception, stackTrace) {
print('Error cargando imagen: $exception');
},
)
// Avatar con iniciales
CircleAvatar(
radius: 40,
backgroundColor: Colors.blue,
child: Text(
'JP',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
)
// Avatar con icono
CircleAvatar(
radius: 35,
backgroundColor: Colors.green,
child: Icon(
Icons.person,
color: Colors.white,
size: 40,
),
) Puedes usar un ListView.builder para mostrar una lista de avatares. Cada avatar puede tener una imagen, iniciales o un ícono.
class ListaAvatares extends StatelessWidget {
final List<Map<String, String>> usuarios = [
{'nombre': 'Ana García', 'iniciales': 'AG', 'imagen': 'https://picsum.photos/100/100?random=1'},
{'nombre': 'Carlos López', 'iniciales': 'CL', 'imagen': 'https://picsum.photos/100/100?random=2'},
{'nombre': 'María Rodríguez', 'iniciales': 'MR', 'imagen': 'https://picsum.photos/100/100?random=3'},
];
Widget build(BuildContext context) {
return ListView.builder(
itemCount: usuarios.length,
itemBuilder: (context, index) {
final usuario = usuarios[index];
return ListTile(
leading: CircleAvatar(
radius: 25,
backgroundImage: NetworkImage(usuario['imagen']!),
backgroundColor: Colors.blue,
onBackgroundImageError: (exception, stackTrace) {
// Fallback a iniciales si falla la imagen
},
child: usuario['imagen']!.isEmpty
? Text(
usuario['iniciales']!,
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
)
: null,
),
title: Text(usuario['nombre']!),
subtitle: Text('Usuario activo'),
trailing: Icon(Icons.more_vert),
onTap: () {
print('Perfil de ${usuario['nombre']}');
},
);
},
);
}
} El widget Hero permite crear animaciones de transición entre dos widgets. Es perfecto para hacer que la transición entre pantallas sea más suave.
// Página de origen
class PaginaOrigen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Página Origen')),
body: Center(
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => PaginaDestino()),
);
},
child: Hero(
tag: 'imagen-hero',
child: ClipRRect(
borderRadius: BorderRadius.circular(15),
child: Image.network(
'https://picsum.photos/200/200',
width: 200,
height: 200,
fit: BoxFit.cover,
),
),
),
),
),
);
}
}
// Página de destino
class PaginaDestino extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Página Destino')),
body: Center(
child: Hero(
tag: 'imagen-hero', // Mismo tag que en origen
child: ClipRRect(
borderRadius: BorderRadius.circular(25),
child: Image.network(
'https://picsum.photos/200/200',
width: 300,
height: 300,
fit: BoxFit.cover,
),
),
),
),
);
}
} El widget FlutterLogo es una forma sencilla de mostrar el logo oficial de Flutter. Puedes personalizar su tamaño, estilo y color.
Column(
children: [
// Logo básico
FlutterLogo(
size: 100,
),
SizedBox(height: 20),
// Logo con estilo personalizado
FlutterLogo(
size: 150,
style: FlutterLogoStyle.horizontal,
textColor: Colors.blue,
),
SizedBox(height: 20),
// Logo solo marca
FlutterLogo(
size: 80,
style: FlutterLogoStyle.markOnly,
),
],
) Esta es una galería de fotos que muestra una lista de imágenes. Puedes hacer clic en cualquier imagen para verla en detalle.
class GaleriaFotos extends StatefulWidget {
_GaleriaFotosState createState() => _GaleriaFotosState();
}
class _GaleriaFotosState extends State<GaleriaFotos> {
final List<String> fotos = List.generate(
20,
(index) => 'https://picsum.photos/300/300?random=$index'
);
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200.0,
floating: false,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text('Mi Galería'),
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.purple, Colors.blue],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Center(
child: FlutterLogo(
size: 80,
style: FlutterLogoStyle.white,
),
),
),
),
),
SliverPadding(
padding: EdgeInsets.all(16),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.0,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetalleImagen(
imageUrl: fotos[index],
heroTag: 'foto-$index',
),
),
);
},
child: Hero(
tag: 'foto-$index',
child: ClipRRect(
borderRadius: BorderRadius.circular(15),
child: Stack(
fit: StackFit.expand,
children: [
Image.network(
fotos[index],
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
color: Colors.grey[300],
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
},
),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.7),
],
),
),
),
Positioned(
bottom: 8,
left: 8,
child: Text(
'Foto ${index + 1}',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
);
},
childCount: fotos.length,
),
),
),
],
),
);
}
}
class DetalleImagen extends StatelessWidget {
final String imageUrl;
final String heroTag;
const DetalleImagen({
Key? key,
required this.imageUrl,
required this.heroTag,
}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
iconTheme: IconThemeData(color: Colors.white),
),
body: Center(
child: Hero(
tag: heroTag,
child: InteractiveViewer(
child: ClipRRect(
borderRadius: BorderRadius.circular(15),
child: Image.network(
imageUrl,
fit: BoxFit.contain,
),
),
),
),
),
);
}
} Tip de Performance: Los Slivers son ideales para crear scroll effects complejos sin sacrificar rendimiento. Úsalos cuando necesites comportamientos de scroll personalizados.
Hero Animations: Siempre usa tags únicos para las animaciones Hero. Esto evita conflictos y asegura transiciones suaves entre pantallas.
class ImagenOptimizada extends StatelessWidget {
final String imageUrl;
final double? width;
final double? height;
const ImagenOptimizada({
Key? key,
required this.imageUrl,
this.width,
this.height,
}) : super(key: key);
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
imageUrl,
width: width,
height: height,
fit: BoxFit.cover,
// Placeholder mientras carga
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
width: width,
height: height,
color: Colors.grey[300],
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
},
// Manejo de errores
errorBuilder: (context, error, stackTrace) {
return Container(
width: width,
height: height,
color: Colors.grey[300],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, color: Colors.red),
SizedBox(height: 8),
Text('Error al cargar imagen'),
],
),
);
},
// Cache de red
cacheWidth: width?.toInt(),
cacheHeight: height?.toInt(),
),
);
}
} class ScrollOptimizado extends StatelessWidget {
Widget build(BuildContext context) {
return CustomScrollView(
// Mejora el rendimiento en listas largas
cacheExtent: 500,
slivers: [
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text('Scroll Optimizado'),
),
),
// Usa SliverList.builder para mejor rendimiento
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text('Item $index'),
subtitle: Text('Descripción del item'),
);
},
childCount: 1000,
// Mejora el rendimiento con addAutomaticKeepAlives
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
),
),
],
);
}
}