tutoriales.com

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.

Principiante25 min de lectura7 views
Reportar error

🚀 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.

🔥 Importante: Comprender la reutilización de widgets y la aplicación de temas es crucial para cualquier desarrollador Flutter que aspire a construir aplicaciones de calidad profesional. Estos conceptos son la base de un diseño limpio y una UI coherente.

✨ ¿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.

📌 Nota: Los sistemas de diseño como Material Design de Google o Cupertino de Apple son ejemplos de cómo la definición de componentes y temas preestablecidos facilita la construcción de UI coherentes y funcionales. Flutter nos da las herramientas para implementar nuestros propios sistemas de diseño personalizados.

🛠️ 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 que StatelessWidget es 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). El required en 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 el primaryColor del tema actual para backgroundColor.
  • ElevatedButton: Utilizamos el widget ElevatedButton de 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!

💡 Consejo: Considera crear una carpeta `components` o `shared` para agrupar widgets que se usarán en toda la aplicación, y subcarpetas para categorías de widgets (e.g., `buttons`, `inputs`, `cards`).

🎨 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.

MaterialApp ThemeData primaryColor: Colors.blue textTheme: TextTheme(...) AppBar ElevatedButton Text Usa primaryColor Usa primaryColor Usa textTheme

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:

  • primaryColor y accentColor: Nuestros colores principales para el tema.
  • lightTheme: Un objeto ThemeData para 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 los ElevatedButton. ¡Esto afectará a nuestro CustomButton!
    • floatingActionButtonTheme y inputDecorationTheme: Ejemplos de cómo personalizar otros widgets.
  • darkTheme: Un objeto ThemeData similar, 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 AppBar tiene el primaryColor y el estilo de texto que definimos en appBarTheme.
  • El primer CustomButton (que no especifica backgroundColor) toma el color de fondo definido en elevatedButtonTheme (nuestro primaryColor).
  • El texto "¡Bienvenido a nuestro tutorial!" ahora usa el headlineLarge de nuestro textTheme.
  • El ElevatedButton estándar también adopta el estilo definido en elevatedButtonTheme.
  • El FloatingActionButton tiene el estilo de floatingActionButtonTheme.

¡La personalización del tema se aplica automáticamente a todos los widgets que hacen uso de él!

💡 Consejo: Usa `Theme.of(context).colorScheme.primary` o `Theme.of(context).textTheme.bodyLarge` para acceder a los valores del tema en cualquier parte de tu árbol de widgets. Esto asegura que tus widgets se adapten dinámicamente al tema actual.

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, subtitle y, crucialmente, un Widget content. Esto permite que el contenido interno de la tarjeta sea totalmente flexible, aceptando cualquier widget (texto, imágenes, listas, etc.).
  • Utiliza Card como su base, que ya proporciona algunas propiedades de Material Design como elevation y shape.
  • Aprovecha Theme.of(context).textTheme y Theme.of(context).colorScheme para 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.

75% Completado

🌐 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.

  1. Crea una carpeta fonts en la raíz de tu proyecto (junto a lib, pubspec.yaml).
  2. Descarga una fuente de Google Fonts (por ejemplo, RobotoCondensed-Regular.ttf) y guárdala en fonts.
  3. 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.

Creación de CustomButton: Encapsulación de un botón simple.
Definición de AppTheme: Configuración global de colores y estilos.
Uso de CustomButton con Tema: Adaptación automática del botón al tema.
Creación de CustomCard: Widget más complejo con contenido flexible y estilo temático.
Integración de Fuentes: Opcional, pero potente para identidad visual.
Creación de CustomInputField: Ejemplificando la herencia de estilos de formulario del tema.

💡 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.
  • lib/shared/widgets/ (widgets reutilizables a nivel de aplicación, como CustomButton, CustomCard)
  • lib/shared/themes/ (definiciones de temas, como AppTheme)

📝 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.

⚠️ Advertencia: Un widget con demasiados parámetros puede volverse difícil de usar y mantener. Busca el equilibrio entre flexibilidad y simplicidad.

🏁 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

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!