Tarjeta de Perfil con Botón de Seguir
import 'package:flutter/material.dart';
void main() {
runApp(const MiAplicacionWeb());
}
// 1. EL CONTENEDOR PRINCIPAL DE LA APP
// Requerido en DartPad para inicializar el diseño Material Design y el lienzo (Scaffold)
class MiAplicacionWeb extends StatelessWidget {
const MiAplicacionWeb({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false, // Quita la banda roja de "Debug"
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
home: Scaffold(
backgroundColor: Colors.grey[100], // Fondo gris claro para que resalte la tarjeta
appBar: AppBar(
title: const Text('Vista previa en DartPad'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
body: const Center(
// Centramos la tarjeta en la pantalla web
child: TarjetaPerfil(),
),
),
);
}
}
// 2. LA ESTRUCTURA DE LA TARJETA (StatelessWidget)
// Distribuye el espacio, los márgenes, bordes redondeados y la sombra
class TarjetaPerfil extends StatelessWidget {
const TarjetaPerfil({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 350, // Limitamos el ancho para que en pantallas de PC no se estire demasiado
padding: const EdgeInsets.all(16.0),
margin: const EdgeInsets.all(20.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 2,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: const Column(
mainAxisSize: MainAxisSize.min, // Se ajusta estrictamente al alto del contenido
children: [
// Fila superior: Avatar + Textos
Row(
children: [
CircleAvatar(
radius: 30,
backgroundColor: Colors.blueAccent,
child: Icon(Icons.person, size: 35, color: Colors.white),
),
SizedBox(width: 15),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Alejandro Magueyal',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
'Desarrollador Flutter',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
],
),
Divider(height: 30, thickness: 1), // Línea separadora
// Componente inferior: Botón Interactivo
BotonSeguir(),
],
),
);
}
}
// 3. EL COMPONENTE INTERACTIVO (StatefulWidget)
// Controla el estado del botón (Seguir / Siguiendo) y se redibuja de forma independiente
class BotonSeguir extends StatefulWidget {
const BotonSeguir({super.key});
@override
State<BotonSeguir> createState() => _BotonSeguirState();
}
class _BotonSeguirState extends State<BotonSeguir> {
bool _siguiendo = false; // Variable de estado
@override
Widget build(BuildContext context) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: _siguiendo ? Colors.grey[300] : Colors.blue,
foregroundColor: _siguiendo ? Colors.black87 : Colors.white,
minimumSize: const Size(double.infinity, 45), // Ancho completo dentro de la tarjeta
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // Bordes ligeramente redondeados
),
),
onPressed: () {
setState(() {
_siguiendo = !_siguiendo; // Cambia el estado y avisa a Flutter que redibuje
});
},
child: Text(
_siguiendo ? 'Siguiendo' : 'Seguir',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
);
}
}
2do Ejemplo
import 'package:flutter/material.dart';
void main() {
runApp(const MiSegundoEjercicio());
}
class MiSegundoEjercicio extends StatelessWidget {
const MiSegundoEjercicio({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
home: Scaffold(
backgroundColor: Colors.grey[200],
appBar: AppBar(
title: const Text('Ejercicio 2: Layouts e Iconos'),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
body: const Center(
child: TarjetaMetricas(),
),
),
);
}
}
// 1. LA TARJETA PRINCIPAL (Organiza secciones verticalmente)
class TarjetaMetricas extends StatelessWidget {
const TarjetaMetricas({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 360,
padding: const EdgeInsets.all(20.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: const Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Estadísticas de Cuenta',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.indigo),
),
SizedBox(height: 20),
// SECCIÓN 1: Fila de Métricas (Uso de Row distribuido)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // Distribuye el espacio equitativamente
children: [
ColumnaMetrica(icono: Icons.folder_open, valor: '12', etiqueta: 'Proyectos'),
ColumnaMetrica(icono: Icons.star_border, valor: '4.9', etiqueta: 'Rating'),
ColumnaMetrica(icono: Icons.remove_red_eye_outlined, valor: '1.2k', etiqueta: 'Vistas'),
],
),
Divider(height: 40, thickness: 1),
// SECCIÓN 2: Ajustes interactivos (Uso de StatefulWidgets)
ElementoAjusteInformativo(),
],
),
);
}
}
// 2. WIDGET REUTILIZABLE: Para no repetir código en la fila de métricas
class ColumnaMetrica extends StatelessWidget {
final IconData icono;
final String valor;
final String etiqueta;
const ColumnaMetrica({
super.key,
required this.icono,
required this.valor,
required this.etiqueta,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Icon(icono, color: Colors.indigo, size: 28),
const SizedBox(height: 6),
Text(valor, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Text(etiqueta, style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
);
}
}
// 3. SECCIÓN INTERACTIVA: Un interruptor para activar/desactivar notificaciones
class ElementoAjusteInformativo extends StatefulWidget {
const ElementoAjusteInformativo({super.key});
@override
State<ElementoAjusteInformativo> createState() => _ElementoAjusteInformativoState();
}
class _ElementoAjusteInformativoState extends State<ElementoAjusteInformativo> {
bool _notificacionesActivas = true; // Estado local del interruptor
@override
Widget build(BuildContext context) {
// ListTile es un widget excelente que ya viene con la estructura: Icono izquierdo + Título + Componente derecho
return ListTile(
contentPadding: EdgeInsets.zero, // Quita los márgenes internos por defecto
leading: Icon(
_notificacionesActivas ? Icons.notifications_active : Icons.notifications_off,
color: _notificacionesActivas ? Colors.green : Colors.grey,
),
title: const Text('Notificaciones push'),
subtitle: Text(_notificacionesActivas ? 'Activadas' : 'Silenciadas'),
trailing: Switch(
value: _notificacionesActivas,
onChanged: (nuevoValor) {
setState(() {
_notificacionesActivas = nuevoValor; // Actualiza la pantalla al presionar el switch
});
},
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(const MiTercerEjercicio());
}
class MiTercerEjercicio extends StatelessWidget {
const MiTercerEjercicio({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: Colors.teal,
),
home: Scaffold(
backgroundColor: Colors.teal[50],
appBar: AppBar(
title: const Text('Ejercicio 3: Campos de Texto'),
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
),
body: const Center(
child: FormularioLogin(),
),
),
);
}
}
// EL FORMULARIO INTERACTIVO (Debe ser StatefulWidget para capturar el texto y controlar la visibilidad)
class FormularioLogin extends StatefulWidget {
const FormularioLogin({super.key});
@override
State<FormularioLogin> createState() => _FormularioLoginState();
}
class _FormularioLoginState extends State<FormularioLogin> {
// 1. CONTROLADORES: Son los encargados de "escuchar" y extraer el texto de los inputs
final TextEditingController _controladorEmail = TextEditingController();
final TextEditingController _controladorPassword = TextEditingController();
// Variable de estado para ocultar/mostrar la contraseña (ojo de la contraseña)
bool _ocultarPassword = true;
// Liberamos los controladores de la memoria cuando el widget se destruye (Buena práctica)
@override
void dispose() {
_controladorEmail.dispose();
_controladorPassword.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: 340,
padding: const EdgeInsets.all(24.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20.0),
boxShadow: [
BoxShadow(
color: Colors.teal.withOpacity(0.15),
blurRadius: 15,
offset: const Offset(0, 8),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, // Hace que los botones y campos ocupen todo el ancho
children: [
const Text(
'Crear Cuenta',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.teal),
),
const SizedBox(height: 25),
// CAMPO 1: CORREO ELECTRÓNICO
TextField(
controller: _controladorEmail,
keyboardType: TextInputType.emailAddress, // Optimiza el teclado móvil con el símbolo '@'
decoration: InputDecoration(
labelText: 'Correo Electrónico',
hintText: 'ejemplo@correo.com',
prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 20),
// CAMPO 2: CONTRASEÑA
TextField(
controller: _controladorPassword,
obscureText: _ocultarPassword, // Si es true, transforma el texto en puntitos (••••)
decoration: InputDecoration(
labelText: 'Contraseña',
prefixIcon: const Icon(Icons.lock_outline),
// Ponemos un botón interactivo dentro del mismo input para mostrar/ocultar texto
suffixIcon: IconButton(
icon: Icon(_ocultarPassword ? Icons.visibility_off : Icons.visibility),
onPressed: () {
setState(() {
_ocultarPassword = !_ocultarPassword; // Invierte el estado físico del ojo
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 30),
// BOTÓN DE ACCIÓN
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
// Obtenemos el texto actual directamente desde los controladores
String emailInput = _controladorEmail.text;
String passwordInput = _controladorPassword.text;
// Mostramos un mensaje flotante (SnackBar) con los datos capturados
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Registrando a: $emailInput (Pass: $passwordInput)'),
backgroundColor: Colors.teal[700],
),
);
},
child: const Text('Registrarse', style: TextStyle(fontSize: 16)),
),
],
),
);
}
}