Flutter para Principiantes: Creando Widgets Reutilizables y Temas Personalizados
Este tutorial te guiará paso a paso en la creación de widgets reutilizables y la implementación de temas personalizados en tus aplicaciones Flutter. Descubre cómo mantener una UI consistente, escalable y fácil de mantener.
🚀 Introducción a los Widgets Reutilizables y Temas en Flutter
Flutter, el potente framework de UI de Google, se basa en el concepto de widgets para construir interfaces de usuario. Todo en Flutter es un widget: desde la estructura de la aplicación hasta el texto, los botones y las animaciones. Esta arquitectura centrada en widgets fomenta la modularidad y la reutilización, aspectos clave para desarrollar aplicaciones escalables y mantenibles.
En este tutorial, profundizaremos en dos pilares fundamentales del desarrollo eficiente con Flutter: la creación de widgets reutilizables y la implementación de temas personalizados. La capacidad de encapsular lógica y UI en componentes autónomos (widgets reutilizables) y de definir un estilo visual uniforme en toda la aplicación (temas) no solo acelera el desarrollo, sino que también garantiza una experiencia de usuario consistente y una base de código más robusta.
Imagina que estás construyendo una aplicación con docenas de botones, campos de texto y tarjetas de información. Si cada uno de estos elementos se define de forma independiente, cualquier cambio en el diseño (por ejemplo, cambiar el color principal de los botones o el borde de las tarjetas) requeriría modificar cada instancia individualmente. Esto es propenso a errores y consume mucho tiempo. Aquí es donde los widgets reutilizables y los temas brillan con luz propia, permitiéndonos definir estos estilos una sola vez y aplicarlos globalmente o en múltiples lugares de manera eficiente.
✨ ¿Por qué son tan importantes los Widgets Reutilizables y los Temas?
La importancia de estas prácticas se manifiesta en varios frentes, impactando positivamente tanto el proceso de desarrollo como el producto final.
✅ Consistencia de la UI/UX
Uno de los mayores desafíos en el desarrollo de aplicaciones es mantener una interfaz de usuario consistente. Los usuarios esperan que los elementos visuales se comporten y se vean de manera similar en toda la aplicación. Los widgets reutilizables y los temas garantizan que todos los botones, textos, campos de entrada y otros elementos compartan el mismo estilo, color, fuente y comportamiento. Esto no solo mejora la estética, sino que también facilita la comprensión y la navegación para el usuario, creando una experiencia más fluida y profesional.
⏱️ Eficiencia en el Desarrollo
Crear un componente una sola vez y usarlo en múltiples lugares reduce drásticamente la cantidad de código repetitivo. Esto se traduce en un menor tiempo de desarrollo, ya que no tienes que reescribir la misma lógica o diseño una y otra vez. Además, cualquier cambio o mejora en el widget o tema se propaga automáticamente a todas sus instancias, ahorrando innumerables horas de trabajo manual.
🐞 Facilidad de Mantenimiento y Depuración
Cuando la UI de tu aplicación se construye a partir de componentes bien definidos y temáticos, el mantenimiento se vuelve mucho más sencillo. Si hay un error en un widget personalizado, sabes exactamente dónde buscar y corregirlo. De manera similar, si necesitas actualizar el diseño de toda la aplicación, simplemente modificas el tema o los widgets base, y los cambios se aplican globalmente. Esto contrasta fuertemente con un enfoque sin reutilización, donde un pequeño cambio podría requerir ajustes en docenas o cientos de archivos.
📈 Escalabilidad del Proyecto
Las aplicaciones crecen y evolucionan. Un proyecto bien estructurado con widgets reutilizables y un sistema de temas es mucho más fácil de escalar. A medida que se añaden nuevas funcionalidades o pantallas, puedes seguir aprovechando los componentes existentes o crear nuevos basándote en la guía de estilo definida por el tema. Esto asegura que la aplicación pueda crecer sin caer en un laberinto de código duplicado e inconsistencias visuales.
🤝 Colaboración en Equipo
En un equipo de desarrollo, la consistencia y la claridad son vitales. Al establecer una biblioteca de widgets reutilizables y un sistema de temas, los desarrolladores pueden trabajar con una base común. Esto reduce la fricción, asegura que todos sigan las mismas pautas de diseño y facilita la integración del trabajo de diferentes miembros del equipo.
🛠️ Entorno de Desarrollo
Para seguir este tutorial, necesitarás tener instalado Flutter en tu máquina. Si aún no lo has hecho, puedes visitar la documentación oficial de Flutter para obtener instrucciones detalladas.
Necesitarás:
- Flutter SDK: Versión 3.x o superior recomendada.
- Dart SDK: Viene incluido con Flutter.
- IDE: Visual Studio Code con la extensión de Flutter/Dart, o Android Studio con el plugin de Flutter/Dart.
- Dispositivo/Emulador: Un emulador de Android/iOS o un dispositivo físico para ejecutar y probar la aplicación.
Para verificar tu instalación, abre tu terminal y ejecuta:
flutter doctor
Asegúrate de que no haya problemas reportados y que todos los requisitos estén marcados con una ✔️.
🧩 Creando Nuestro Primer Widget Reutilizable
Empezaremos creando un widget simple pero efectivo: un CustomButton que tenga un estilo uniforme en toda la aplicación. Este botón tendrá un texto, un color de fondo y una acción al ser presionado.
Paso 1: Configuración Inicial del Proyecto
Crea un nuevo proyecto de Flutter si no tienes uno ya:
flutter create reusable_widgets_themes_app
cd reusable_widgets_themes_app
Abre el proyecto en tu IDE preferido. Vamos a limpiar el archivo main.dart para empezar desde cero. Reemplaza todo el contenido de lib/main.dart con lo siguiente:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Widgets Reutilizables y Temas',
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Inicio'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'¡Bienvenido a nuestro tutorial!',
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 20),
// Aquí añadiremos nuestro CustomButton
],
),
),
);
}
}
Paso 2: Creando el Archivo para el Widget
Es una buena práctica organizar tus widgets personalizados en directorios separados. Crea un nuevo directorio widgets dentro de lib, y dentro de este, un archivo custom_button.dart.
lib/widgets/custom_button.dart:
import 'package:flutter/material.dart';
class CustomButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
final Color? backgroundColor;
final Color? textColor;
const CustomButton({
super.key,
required this.text,
required this.onPressed,
this.backgroundColor,
this.textColor,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor ?? Theme.of(context).primaryColor, // Usar primaryColor del tema si no se especifica
foregroundColor: textColor ?? Colors.white, // Color del texto
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // Bordes redondeados
),
),
child: Text(
text,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
);
}
}
Analicemos este código:
CustomButton extends StatelessWidget: Nuestro botón no necesita estado interno que cambie, por lo queStatelessWidgetes adecuado.final String text;,final VoidCallback onPressed;: Estas son las propiedades que el widget requiere para funcionar (el texto a mostrar y la acción al ser presionado). Elrequireden el constructor asegura que estas propiedades sean proporcionadas.final Color? backgroundColor;,final Color? textColor;: Estas propiedades son opcionales (?). Si no se proporcionan, usaremos valores predeterminados, incluyendo elprimaryColordel tema actual parabackgroundColor.ElevatedButton: Utilizamos el widgetElevatedButtonde Material Design como base para nuestro botón personalizado.ElevatedButton.styleFrom: Permite personalizar el estilo del botón. Aquí estamos configurando el color de fondo, el color del texto, el padding y la forma del borde.
Paso 3: Usando el Widget Reutilizable
Ahora, volvamos a main.dart e importemos y usemos nuestro CustomButton. Reemplaza el comentario // Aquí añadiremos nuestro CustomButton con lo siguiente:
// ... dentro de HomeScreen Column children
CustomButton(
text: 'Botón Principal',
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('¡Botón principal presionado!')),
);
},
),
const SizedBox(height: 10),
CustomButton(
text: 'Botón Secundario',
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('¡Botón secundario presionado!')),
);
},
backgroundColor: Colors.blueGrey, // Anulando el color por defecto
textColor: Colors.amberAccent, // Anulando el color del texto por defecto
),
No olvides añadir la importación al inicio de main.dart:
import 'package:reusable_widgets_themes_app/widgets/custom_button.dart';
Ejecuta tu aplicación. Verás dos botones, el primero con el color primario por defecto (azul en Material Design por defecto) y el segundo con los colores personalizados que le pasamos. ¡Felicidades, has creado tu primer widget reutilizable!
🎨 Implementando Temas Personalizados en Flutter
Ahora que tenemos un widget reutilizable, es el momento de llevar la personalización al siguiente nivel con temas. Flutter facilita la definición de un tema global para toda tu aplicación, lo que te permite controlar la paleta de colores, la tipografía, las formas de los componentes y mucho más desde un solo lugar.
El Widget ThemeData
El widget MaterialApp tiene una propiedad theme que acepta un objeto ThemeData. Aquí es donde defines los atributos visuales de tu aplicación. Puedes definir colores primarios, secundarios, colores de fondo, estilos de texto (textTheme), y estilos para widgets específicos como AppBar, ElevatedButton, FloatingActionButton, etc.
Paso 1: Definiendo Nuestro Tema
Vamos a crear un archivo separado para nuestros temas. Esto ayuda a mantener el código organizado. Crea un nuevo directorio themes dentro de lib, y dentro de este, un archivo app_theme.dart.
lib/themes/app_theme.dart:
import 'package:flutter/material.dart';
class AppTheme {
// Paleta de colores personalizada
static const Color primaryColor = Color(0xFF673AB7); // Morado oscuro
static const Color accentColor = Color(0xFFE040FB); // Morado claro/rosa
static const Color secondaryTextColor = Color(0xFF757575);
// Tema claro
static ThemeData lightTheme = ThemeData(
brightness: Brightness.light,
primaryColor: primaryColor,
scaffoldBackgroundColor: Colors.white, // Fondo de la pantalla
appBarTheme: const AppBarTheme(
color: primaryColor,
foregroundColor: Colors.white, // Color del texto y íconos en el AppBar
elevation: 5,
centerTitle: true,
titleTextStyle: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
colorScheme: ColorScheme.fromSwatch().copyWith(
primary: primaryColor,
secondary: accentColor, // Color de acento
),
textTheme: const TextTheme(
headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.black87), // Ejemplo de estilo de texto
bodyLarge: TextStyle(fontSize: 16, color: Colors.black87), // Ejemplo de estilo de texto
bodyMedium: TextStyle(fontSize: 14, color: Colors.black54), // Ejemplo de estilo de texto
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor, // Color de fondo del botón
foregroundColor: Colors.white, // Color del texto del botón
padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 12), // Padding por defecto
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), // Bordes redondeados por defecto
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
// Añadir más personalizaciones para otros widgets aquí
// Por ejemplo, para FloatingActionButton:
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: accentColor,
foregroundColor: Colors.white,
elevation: 6,
),
// Para campos de texto (InputDecorationTheme):
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: primaryColor.withOpacity(0.5)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: primaryColor, width: 2),
),
labelStyle: const TextStyle(color: primaryColor),
hintStyle: TextStyle(color: secondaryTextColor.withOpacity(0.7)),
),
);
// Opcionalmente, puedes definir un tema oscuro también
static ThemeData darkTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFFBB86FC), // Un morado más claro para fondo oscuro
scaffoldBackgroundColor: const Color(0xFF121212), // Fondo oscuro
appBarTheme: const AppBarTheme(
color: Color(0xFF1F1F1F),
foregroundColor: Colors.white,
elevation: 5,
centerTitle: true,
titleTextStyle: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
colorScheme: ColorScheme.fromSwatch().copyWith(
primary: const Color(0xFFBB86FC),
secondary: const Color(0xFF03DAC6), // Color de acento verde azulado
brightness: Brightness.dark,
),
textTheme: const TextTheme(
headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white70),
bodyLarge: TextStyle(fontSize: 16, color: Colors.white70),
bodyMedium: TextStyle(fontSize: 14, color: Colors.white54),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFBB86FC), // Color de fondo del botón
foregroundColor: Colors.black, // Texto negro en botón morado
padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: Color(0xFF03DAC6),
foregroundColor: Colors.black,
elevation: 6,
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: const Color(0xFFBB86FC).withOpacity(0.5)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFFBB86FC), width: 2),
),
labelStyle: const TextStyle(color: Color(0xFFBB86FC)),
hintStyle: TextStyle(color: secondaryTextColor.withOpacity(0.7)),
),
);
}
En AppTheme hemos definido:
primaryColoryaccentColor: Nuestros colores principales para el tema.lightTheme: Un objetoThemeDatapara el tema claro. Aquí personalizamos:brightness: Para indicar que es un tema claro.primaryColor: Color principal para la aplicación.scaffoldBackgroundColor: Color de fondo de las pantallas.appBarTheme: Estilo para todas las barras de aplicación.colorScheme: Paleta de colores general, importante para widgets que usan esto directamente.textTheme: Definición de estilos de texto para diferentes tamaños y usos.elevatedButtonTheme: Estilo por defecto para todos losElevatedButton. ¡Esto afectará a nuestroCustomButton!floatingActionButtonThemeyinputDecorationTheme: Ejemplos de cómo personalizar otros widgets.
darkTheme: Un objetoThemeDatasimilar, pero adaptado para un modo oscuro.
Paso 2: Aplicando el Tema a la Aplicación
Ahora, en main.dart, importaremos app_theme.dart y aplicaremos el tema a nuestra aplicación. Modifica la clase MyApp:
import 'package:flutter/material.dart';
import 'package:reusable_widgets_themes_app/widgets/custom_button.dart';
import 'package:reusable_widgets_themes_app/themes/app_theme.dart'; // Importar nuestro tema
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Widgets Reutilizables y Temas',
theme: AppTheme.lightTheme, // Aplicar nuestro tema claro
// Para habilitar el cambio entre temas oscuro/claro, usaríamos themeMode y darkTheme
// themeMode: ThemeMode.system, // O ThemeMode.dark, ThemeMode.light
// darkTheme: AppTheme.darkTheme,
home: const HomeScreen(),
);
}
}
// HomeScreen se mantiene igual por ahora
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Inicio'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'¡Bienvenido a nuestro tutorial!',
style: Theme.of(context).textTheme.headlineLarge, // Usar estilo del tema
),
const SizedBox(height: 20),
CustomButton(
text: 'Botón con Tema',
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('¡Botón temático presionado!')),
);
},
// Ya no necesitamos especificar backgroundColor si el tema lo maneja
// o si queremos anular el color por defecto del tema, podemos hacerlo aquí.
),
const SizedBox(height: 10),
CustomButton(
text: 'Botón con Anulación',
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('¡Botón con anulación presionado!')),
);
},
backgroundColor: AppTheme.accentColor, // Anulando con un color de nuestro tema
textColor: Colors.black, // Texto negro para el botón de acento
),
const SizedBox(height: 10),
// Ejemplo de un botón sin usar CustomButton para ver el estilo por defecto del tema
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('¡ElevatedButton normal!')),
);
},
child: const Text('ElevatedButton estándar'),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('FAB presionado!')),
);
},
child: const Icon(Icons.add),
),
);
}
}
Ejecuta la aplicación. Ahora verás que:
- La
AppBartiene elprimaryColory el estilo de texto que definimos enappBarTheme. - El primer
CustomButton(que no especificabackgroundColor) toma el color de fondo definido enelevatedButtonTheme(nuestroprimaryColor). - El texto "¡Bienvenido a nuestro tutorial!" ahora usa el
headlineLargede nuestrotextTheme. - El
ElevatedButtonestándar también adopta el estilo definido enelevatedButtonTheme. - El
FloatingActionButtontiene el estilo defloatingActionButtonTheme.
¡La personalización del tema se aplica automáticamente a todos los widgets que hacen uso de él!
Paso 3: Probando el Modo Oscuro
Si deseas probar el modo oscuro, simplemente cambia la propiedad theme en MaterialApp a AppTheme.darkTheme. Para una experiencia más completa, puedes configurar themeMode para que el sistema decida o para un cambio manual:
// ... dentro de MyApp build method
return MaterialApp(
title: 'Widgets Reutilizables y Temas',
theme: AppTheme.lightTheme, // Tema claro por defecto
darkTheme: AppTheme.darkTheme, // Tema oscuro
themeMode: ThemeMode.system, // Utiliza el tema del sistema (claro u oscuro)
home: const HomeScreen(),
);
Si tu sistema operativo está en modo oscuro, la aplicación ahora se mostrará con el darkTheme que definimos. ¡Esto es increíble para la accesibilidad y la preferencia del usuario!
🔄 Creando Widgets Más Complejos y Reutilizables
Un botón es un buen comienzo, pero la verdadera potencia de los widgets reutilizables se manifiesta con componentes más elaborados. Crearemos una CustomCard que pueda mostrar un título, un subtítulo y un contenido personalizado.
Paso 1: Diseñando la CustomCard
Crea un nuevo archivo custom_card.dart dentro de lib/widgets.
lib/widgets/custom_card.dart:
import 'package:flutter/material.dart';
class CustomCard extends StatelessWidget {
final String title;
final String subtitle;
final Widget content;
final EdgeInsetsGeometry? padding;
const CustomCard({
super.key,
required this.title,
required this.subtitle,
required this.content,
this.padding,
});
@override
Widget build(BuildContext context) {
return Card(
margin: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 4, // Sombra para darle un efecto 3D
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), // Bordes redondeados
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
), // Estilo del tema para el título
),
const SizedBox(height: 5),
Text(
subtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: AppTheme.secondaryTextColor, // Color secundario definido en nuestro tema
),
),
const Divider(height: 20, thickness: 1),
content, // Contenido personalizado
],
),
),
);
}
}
Observa en este CustomCard:
- Recibe
title,subtitley, crucialmente, unWidget content. Esto permite que el contenido interno de la tarjeta sea totalmente flexible, aceptando cualquier widget (texto, imágenes, listas, etc.). - Utiliza
Cardcomo su base, que ya proporciona algunas propiedades de Material Design comoelevationyshape. - Aprovecha
Theme.of(context).textThemeyTheme.of(context).colorSchemepara garantizar que el texto y los colores se adapten al tema actual de la aplicación.
Paso 2: Usando la CustomCard en HomeScreen
Actualiza main.dart para incluir nuestra nueva tarjeta. Primero, importa el nuevo widget:
import 'package:reusable_widgets_themes_app/widgets/custom_card.dart';
Luego, añade una o dos instancias de CustomCard a la Column en HomeScreen:
// ... dentro de HomeScreen Column children
const SizedBox(height: 20),
CustomCard(
title: 'Tarjeta de Información',
subtitle: 'Este es un ejemplo de una tarjeta personalizada.',
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Aquí puedes poner cualquier tipo de contenido.'),
const SizedBox(height: 5),
Row(
children: [
Icon(Icons.info_outline, color: Theme.of(context).colorScheme.secondary),
const SizedBox(width: 8),
const Text('¡Más detalles aquí!'),
],
),
],
),
),
const SizedBox(height: 20),
CustomCard(
title: 'Otra Tarjeta Personalizada',
subtitle: 'Esta tarjeta muestra un contenido diferente.',
content: Image.network(
'https://picsum.photos/250?random=1',
height: 100,
fit: BoxFit.cover,
),
),
// ... resto de los widgets
Reinicia la aplicación. Verás las CustomCard con el estilo consistente y los contenidos personalizados. Observa cómo el título de la tarjeta hereda el color primario del tema, y el subtítulo usa secondaryTextColor que definimos en AppTheme.
🌐 Extendiendo la Personalización: Temas de Texto y Fuentes
Además de los colores, la tipografía juega un papel crucial en el diseño de una aplicación. Flutter permite personalizar las fuentes y los estilos de texto de manera granular a través de textTheme.
Paso 1: Añadiendo una Fuente Personalizada (Opcional)
Para usar fuentes personalizadas, primero debes añadir el archivo de la fuente (.ttf o .otf) a tu proyecto.
- Crea una carpeta
fontsen la raíz de tu proyecto (junto alib,pubspec.yaml). - Descarga una fuente de Google Fonts (por ejemplo,
RobotoCondensed-Regular.ttf) y guárdala enfonts. - Declara la fuente en
pubspec.yaml:
flutter:
uses-material-design: true
fonts:
- family: RobotoCondensed
fonts:
- asset: fonts/RobotoCondensed-Regular.ttf
- asset: fonts/RobotoCondensed-Bold.ttf # Si tienes versiones en negrita, etc.
weight: 700
Ejecuta `flutter pub get` después de modificar `pubspec.yaml`.
Paso 2: Usando textTheme en app_theme.dart
Ya hemos visto textTheme brevemente. Ahora lo extenderemos y haremos referencia a nuestra fuente personalizada si la añadimos. Modifica AppTheme.lightTheme y AppTheme.darkTheme en lib/themes/app_theme.dart.
// ... dentro de AppTheme.lightTheme
textTheme: const TextTheme(
displayLarge: TextStyle(fontFamily: 'RobotoCondensed', fontSize: 57, fontWeight: FontWeight.bold, color: Colors.black87), // Ejemplo de estilo de texto
headlineLarge: TextStyle(fontFamily: 'RobotoCondensed', fontSize: 32, fontWeight: FontWeight.bold, color: Colors.black87), // Ejemplo de estilo de texto
titleLarge: TextStyle(fontFamily: 'RobotoCondensed', fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
bodyLarge: TextStyle(fontSize: 16, color: Colors.black87), // Ejemplo de estilo de texto
bodyMedium: TextStyle(fontSize: 14, color: Colors.black54), // Ejemplo de estilo de texto
).apply(displayColor: Colors.black87, bodyColor: Colors.black87), // Aplicar un color base para todo el textTheme
// ...
Repite un proceso similar para darkTheme, ajustando los colores del texto para contrastar con el fondo oscuro.
// ... dentro de AppTheme.darkTheme
textTheme: const TextTheme(
displayLarge: TextStyle(fontFamily: 'RobotoCondensed', fontSize: 57, fontWeight: FontWeight.bold, color: Colors.white70),
headlineLarge: TextStyle(fontFamily: 'RobotoCondensed', fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white70),
titleLarge: TextStyle(fontFamily: 'RobotoCondensed', fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white70),
bodyLarge: TextStyle(fontSize: 16, color: Colors.white70),
bodyMedium: TextStyle(fontSize: 14, color: Colors.white54),
).apply(displayColor: Colors.white70, bodyColor: Colors.white70),
// ...
Ahora, en cualquier lugar de tu aplicación, puedes acceder a estos estilos de texto temáticos:
Text(
'Título con fuente personalizada',
style: Theme.of(context).textTheme.headlineLarge, // Usará RobotoCondensed si se definió
),
Text(
'Cuerpo de texto',
style: Theme.of(context).textTheme.bodyLarge,
),
Al usar los estilos de textTheme, garantizamos que toda la tipografía de la aplicación se adhiera a las pautas de diseño establecidas en el tema, incluso si decidimos cambiar la fuente o los tamaños de forma global en el futuro.
¿Qué es `.apply(displayColor: ..., bodyColor: ...)`?
Esta función es útil para aplicar un color general a todo el `TextTheme` después de definir los estilos individuales. `displayColor` se aplica a los estilos de texto más grandes (como `displayLarge`, `headlineLarge`, `titleLarge`), mientras que `bodyColor` se aplica a los estilos de texto más pequeños (como `bodyLarge`, `bodyMedium`, `labelLarge`, etc.). Esto evita tener que especificar el color en cada `TextStyle` individualmente si la mayoría comparten el mismo color base.🎯 Combinando Widgets Reutilizables y Temas: Un Ejemplo Práctico
Vamos a crear un formulario de entrada de datos simple, utilizando un widget de entrada de texto personalizado que herede los estilos de nuestro tema.
Paso 1: Creando CustomInputField
Crea un nuevo archivo custom_input_field.dart dentro de lib/widgets.
lib/widgets/custom_input_field.dart:
import 'package:flutter/material.dart';
class CustomInputField extends StatelessWidget {
final String labelText;
final String hintText;
final IconData? icon;
final bool obscureText;
final TextEditingController? controller;
final String? Function(String?)? validator;
final TextInputType? keyboardType;
const CustomInputField({
super.key,
required this.labelText,
this.hintText = '',
this.icon,
this.obscureText = false,
this.controller,
this.validator,
this.keyboardType,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: TextFormField(
controller: controller,
obscureText: obscureText,
keyboardType: keyboardType,
decoration: InputDecoration(
labelText: labelText,
hintText: hintText,
prefixIcon: icon != null ? Icon(icon, color: Theme.of(context).colorScheme.primary) : null,
border: Theme.of(context).inputDecorationTheme.border, // Usar el borde del tema
focusedBorder: Theme.of(context).inputDecorationTheme.focusedBorder, // Usar el borde enfocado del tema
labelStyle: Theme.of(context).inputDecorationTheme.labelStyle, // Usar el estilo del label del tema
hintStyle: Theme.of(context).inputDecorationTheme.hintStyle, // Usar el estilo del hint del tema
contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 12),
),
validator: validator,
),
);
}
}
Este CustomInputField es un TextFormField envuelto, que personaliza su InputDecoration utilizando los estilos definidos en Theme.of(context).inputDecorationTheme.
Paso 2: Integrando CustomInputField en HomeScreen
Primero, importa el nuevo widget en main.dart:
import 'package:reusable_widgets_themes_app/widgets/custom_input_field.dart';
Ahora, añade algunos CustomInputField a la Column de tu HomeScreen. Puedes colocarlos después de las CustomCards.
// ... dentro de HomeScreen Column children
const SizedBox(height: 20),
CustomInputField(
labelText: 'Nombre de usuario',
hintText: 'Introduce tu nombre',
icon: Icons.person,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Por favor introduce un nombre de usuario';
}
return null;
},
),
CustomInputField(
labelText: 'Contraseña',
hintText: 'Introduce tu contraseña',
icon: Icons.lock,
obscureText: true,
keyboardType: TextInputType.text,
),
const SizedBox(height: 20),
CustomButton(
text: 'Enviar Formulario',
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Formulario enviado! (Validación no implementada en este ejemplo)')),
);
},
),
// ... resto de los widgets
Reinicia la aplicación. Verás los campos de entrada de texto con el borde redondeado, el color de etiqueta y el ícono personalizado, todo heredado de nuestro inputDecorationTheme definido en app_theme.dart. Si cambias el tema de claro a oscuro, los colores del InputDecoration se adaptarán automáticamente.
💡 Patrones Avanzados y Mejores Prácticas
Para llevar tus habilidades con widgets reutilizables y temas al siguiente nivel, considera estas mejores prácticas:
📦 Organiza Tu Código Cuidadosamente
Como hemos visto, usar directorios como lib/widgets y lib/themes es un buen comienzo. Para proyectos más grandes, podrías tener:
lib/core/(configuraciones globales, constantes)lib/data/(modelos, servicios API)lib/features/(carpetas para cada característica, ej.lib/features/auth/,lib/features/products/)- Dentro de cada característica, puedes tener subdirectorios para
widgets,screens,models, etc., específicos de esa característica.
- Dentro de cada característica, puedes tener subdirectorios para
lib/shared/widgets/(widgets reutilizables a nivel de aplicación, comoCustomButton,CustomCard)lib/shared/themes/(definiciones de temas, comoAppTheme)
📝 Documenta Tus Widgets y Temas
Usa comentarios de Dart (///) para describir la funcionalidad de tus widgets personalizados y las propiedades de tu tema. Esto es invaluable para la mantenibilidad y para que otros desarrolladores (¡o tu yo futuro!) entiendan rápidamente cómo usar y modificar tus componentes.
/// Un botón personalizado con estilos consistentes en toda la aplicación.
/// Permite definir el texto, la acción onPressed, y opcionalmente colores de fondo y texto.
class CustomButton extends StatelessWidget {
// ...
}
⚙️ Considera el Uso de ThemeExtension
Para escenarios más avanzados, donde necesitas añadir propiedades personalizadas a tu tema que no están cubiertas por ThemeData estándar (por ejemplo, colores específicos para ciertos tipos de alertas o sombras personalizadas para elementos no cubiertos por CardTheme), puedes usar ThemeExtension.
Intermedio Pro
// lib/themes/custom_theme_extension.dart
import 'package:flutter/material.dart';
@immutable
class CustomColorsExtension extends ThemeExtension<CustomColorsExtension> {
const CustomColorsExtension({
required this.success,
required this.danger,
});
final Color? success;
final Color? danger;
@override
CustomColorsExtension copyWith({Color? success, Color? danger}) {
return CustomColorsExtension(
success: success ?? this.success,
danger: danger ?? this.danger,
);
}
@override
CustomColorsExtension lerp(
covariant ThemeExtension<CustomColorsExtension>? other,
double t,
) {
if (other is! CustomColorsExtension) {
return this;
}
return CustomColorsExtension(
success: Color.lerp(success, other.success, t),
danger: Color.lerp(danger, other.danger, t),
);
}
// Helper para acceder fácilmente
static CustomColorsExtension of(BuildContext context) => Theme.of(context).extension<CustomColorsExtension>()!;
}
Luego, añadirlo a tu ThemeData:
// ... en app_theme.dart
static ThemeData lightTheme = ThemeData(
// ...
extensions: const <ThemeExtension<dynamic>>[
CustomColorsExtension(
success: Colors.green,
danger: Colors.redAccent,
),
],
);
// ... y cómo usarlo:
// CustomColorsExtension.of(context).success
Esto te permite extender el tema de Flutter con tus propias propiedades personalizadas de manera segura y escalable.
♻️ Refactoriza y Revisa Constantemente
A medida que tu aplicación crece, es posible que identifiques más oportunidades para crear widgets reutilizables o para ajustar tus definiciones de tema. Realiza revisiones periódicas de tu código para asegurarte de que sigues las mejores prácticas y para refactorizar cualquier duplicación que encuentres.
⚠️ Evita la Sobrecarga de Parámetros
Cuando crees widgets reutilizables, ten cuidado de no llenarlos con demasiados parámetros opcionales. Si un widget comienza a tener una docena de parámetros diferentes para cada pequeño ajuste visual, podría ser una señal de que necesitas dividirlo en widgets más pequeños y específicos, o que algunas de esas propiedades deberían manejarse a través del tema global en lugar de individualmente.
🏁 Conclusión
En este tutorial, hemos recorrido el camino desde la creación de un simple CustomButton hasta la implementación de un sistema de temas completo en Flutter. Hemos aprendido la importancia de los widgets reutilizables y los temas para construir aplicaciones robustas, consistentes y fáciles de mantener.
Dominar estos conceptos te permitirá:
- Acelerar el desarrollo al no tener que reescribir código de UI.
- Garantizar la consistencia visual en toda tu aplicación.
- Simplificar el mantenimiento y la evolución del diseño.
- Facilitar la colaboración en equipos de desarrollo.
La arquitectura de widgets de Flutter, combinada con la flexibilidad de su sistema de temas, te proporciona las herramientas para construir interfaces de usuario hermosas y funcionales con una eficiencia excepcional. ¡Ahora tienes las bases para llevar el diseño y la estructura de tus aplicaciones Flutter al siguiente nivel!
Experimenta con diferentes diseños, colores y fuentes. Intenta crear tus propios widgets reutilizables para elementos comunes en tus proyectos, como encabezados de sección, cards de producto, o barras de navegación personalizadas. Cuanto más practiques, más intuitivo te resultará este enfoque modular y temático. ¡Feliz codificación!
Tutoriales relacionados
- Flutter: Implementando Temas Oscuros y Claros Dinámicos con Provider y Shared Preferencesintermediate18 min
- Gestión de Estado Reactiva con BLoC en Flutter: Un Tutorial Completointermediate25 min
- Flutter: Creando un Cliente de Chat en Tiempo Real con WebSockets y Providerintermediate20 min
- Flutter al Detalle: Animaciones Implícitas y Explícitas para Interfaces de Usuario Fluidas y Atractivasintermediate15 min
- Navegación Avanzada en Flutter: Rutas Dinámicas y Deep Linking con GoRouterintermediate18 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!