tutoriales.com

Flutter: Implementando Temas Oscuros y Claros Dinámicos con Provider y Shared Preferences

Este tutorial te guiará paso a paso en la implementación de temas dinámicos (claro y oscuro) en tus aplicaciones Flutter. Aprenderás a usar el paquete `provider` para una gestión de estado eficiente y `shared_preferences` para recordar la preferencia del usuario entre sesiones, creando una experiencia personalizada y moderna.

Intermedio18 min de lectura18 views
Reportar error

Introducción a los Temas Dinámicos en Flutter ✨

Las interfaces de usuario personalizables son cada vez más importantes para mejorar la experiencia del usuario. Una de las características más demandadas es la posibilidad de alternar entre un tema claro y un tema oscuro. Flutter, con su potente sistema de temas, hace que esto sea relativamente sencillo de implementar.

En este tutorial, exploraremos cómo integrar esta funcionalidad utilizando dos paquetes fundamentales en el ecosistema Flutter: provider para la gestión de estado y shared_preferences para la persistencia de datos. Al final, tendrás una aplicación con un selector de tema que recordará tu elección incluso después de cerrar y volver a abrir la app.

🔥 Importante: Asegúrate de tener un entorno de desarrollo Flutter configurado y funcionando antes de empezar. Puedes consultar la documentación oficial de Flutter si necesitas ayuda con la instalación.

Requisitos Previos 🛠️

Para seguir este tutorial, necesitarás:

  • Flutter SDK instalado y configurado.
  • Un editor de código como VS Code o Android Studio.
  • Conocimientos básicos de Flutter y Dart.

Configuración del Proyecto 🚀

Primero, crea un nuevo proyecto Flutter (si aún no tienes uno) y añade las dependencias necesarias.

1. Crear un Nuevo Proyecto

Si ya tienes un proyecto, puedes saltar este paso. Abre tu terminal o línea de comandos y ejecuta:

flutter create flutter_dynamic_themes
cd flutter_dynamic_themes

2. Añadir Dependencias

Necesitaremos dos paquetes: provider para la gestión de estado y shared_preferences para almacenar la preferencia del tema del usuario. Abre tu archivo pubspec.yaml y añade lo siguiente bajo dependencies:

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.0.5
  shared_preferences: ^2.2.2

Guarda el archivo y ejecuta flutter pub get en tu terminal para descargar los paquetes.

💡 Consejo: Siempre es buena práctica verificar las últimas versiones de los paquetes en pub.dev para asegurar compatibilidad y obtener las últimas características.

Paso a Paso: Implementando los Temas 📝

Vamos a desglosar la implementación en varias etapas.

Paso 1: Definir los Temas de la Aplicación 🎨

Flutter nos permite definir temas claros y oscuros de forma declarativa utilizando ThemeData. Crearemos un nuevo archivo, por ejemplo lib/theme_data.dart, para centralizar nuestras definiciones de tema.

import 'package:flutter/material.dart';

class AppThemes {
  static final ThemeData lightTheme = ThemeData(
    brightness: Brightness.light,
    primaryColor: Colors.blue,
    hintColor: Colors.cyan,
    scaffoldBackgroundColor: Colors.grey[100],
    appBarTheme: AppBarTheme(
      backgroundColor: Colors.blue,
      foregroundColor: Colors.white,
    ),
    textTheme: TextTheme(
      bodyLarge: TextStyle(color: Colors.black87),
      bodyMedium: TextStyle(color: Colors.black54),
      titleLarge: TextStyle(color: Colors.black),
    ),
    buttonTheme: ButtonThemeData(
      buttonColor: Colors.blue,
      textTheme: ButtonTextTheme.primary,
    ),
    colorScheme: ColorScheme.light(
      primary: Colors.blue,
      secondary: Colors.cyan,
      surface: Colors.white,
      onSurface: Colors.black87,
    ),
  );

  static final ThemeData darkTheme = ThemeData(
    brightness: Brightness.dark,
    primaryColor: Colors.indigo,
    hintColor: Colors.tealAccent,
    scaffoldBackgroundColor: Colors.grey[900],
    appBarTheme: AppBarTheme(
      backgroundColor: Colors.indigo[700],
      foregroundColor: Colors.white,
    ),
    textTheme: TextTheme(
      bodyLarge: TextStyle(color: Colors.white70),
      bodyMedium: TextStyle(color: Colors.white54),
      titleLarge: TextStyle(color: Colors.white),
    ),
    buttonTheme: ButtonThemeData(
      buttonColor: Colors.indigo,
      textTheme: ButtonTextTheme.primary,
    ),
    colorScheme: ColorScheme.dark(
      primary: Colors.indigo,
      secondary: Colors.tealAccent,
      surface: Colors.grey[850],
      onSurface: Colors.white70,
    ),
  );
}

Aquí hemos definido lightTheme y darkTheme con propiedades distintivas como brightness, primaryColor, scaffoldBackgroundColor y appBarTheme. Puedes personalizar estos valores a tu gusto para que coincidan con la estética de tu aplicación.

Paso 2: Crear el ThemeProvider con ChangeNotifier 📊

Para gestionar el estado del tema de forma reactiva, usaremos provider. Crearemos una clase ThemeProvider que extienda ChangeNotifier. Esta clase será responsable de almacenar el tema actual y notificar a sus oyentes cuando el tema cambie.

Crea un archivo lib/theme_provider.dart:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_dynamic_themes/theme_data.dart'; // Importa tus temas

class ThemeProvider with ChangeNotifier {
  ThemeData _themeData;
  bool _isDarkMode; // Para guardar la preferencia en Shared Preferences

  ThemeProvider(this._themeData, this._isDarkMode);

  ThemeData getTheme() => _themeData;
  bool isDarkMode() => _isDarkMode;

  void toggleTheme() async {
    if (_themeData == AppThemes.lightTheme) {
      _themeData = AppThemes.darkTheme;
      _isDarkMode = true;
    } else {
      _themeData = AppThemes.lightTheme;
      _isDarkMode = false;
    }
    _saveThemePreference(_isDarkMode);
    notifyListeners(); // Notifica a los widgets que el tema ha cambiado
  }

  // Método para cargar la preferencia del tema al iniciar la app
  static Future<ThemeProvider> loadTheme() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    bool isDarkMode = prefs.getBool('isDarkMode') ?? false; // Valor por defecto: claro
    ThemeData initialTheme = isDarkMode ? AppThemes.darkTheme : AppThemes.lightTheme;
    return ThemeProvider(initialTheme, isDarkMode);
  }

  // Método para guardar la preferencia del tema
  void _saveThemePreference(bool isDarkMode) async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setBool('isDarkMode', isDarkMode);
  }
}

Explicación:

  • _themeData: Almacena el ThemeData actual (lightTheme o darkTheme).
  • _isDarkMode: Un bool que indica si el tema actual es oscuro. Esto es crucial para shared_preferences.
  • toggleTheme(): Cambia el _themeData y _isDarkMode, guarda la preferencia y llama a notifyListeners() para reconstruir los widgets que escuchan.
  • loadTheme(): Un método estático asíncrono que recupera la preferencia del tema guardada en SharedPreferences y devuelve una instancia de ThemeProvider inicializada con ese tema.
  • _saveThemePreference(): Guarda la preferencia del tema (true para oscuro, false para claro) en SharedPreferences.

Paso 3: Integrar ThemeProvider en main.dart 🌳

Ahora, necesitamos envolver nuestra aplicación con ChangeNotifierProvider para que ThemeProvider esté disponible para todos los widgets.

Modifica tu archivo lib/main.dart de la siguiente manera:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_dynamic_themes/theme_provider.dart';
import 'package:flutter_dynamic_themes/theme_data.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // Carga la preferencia del tema antes de que la aplicación se inicie
  ThemeProvider themeProvider = await ThemeProvider.loadTheme();
  runApp(MyApp(themeProvider: themeProvider));
}

class MyApp extends StatelessWidget {
  final ThemeProvider themeProvider;

  const MyApp({Key? key, required this.themeProvider}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<ThemeProvider>(
      create: (_) => themeProvider,
      child: Consumer<ThemeProvider>(
        builder: (context, themeProvider, child) {
          return MaterialApp(
            title: 'Temas Dinámicos Flutter',
            debugShowCheckedModeBanner: false,
            theme: themeProvider.getTheme(), // Usa el tema del provider
            home: MyHomePage(),
          );
        },
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final themeProvider = Provider.of<ThemeProvider>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('Temas Dinámicos'),
        actions: [
          IconButton(
            icon: Icon(
              themeProvider.isDarkMode() ? Icons.light_mode : Icons.dark_mode,
            ),
            onPressed: () {
              themeProvider.toggleTheme();
            },
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              '¡Hola, Mundo!',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {},
              child: Text('Un Botón'),
            ),
            SizedBox(height: 20),
            Text(
              'Este es un texto de ejemplo para mostrar los temas.',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            SizedBox(height: 20),
            Switch(
              value: themeProvider.isDarkMode(),
              onChanged: (value) {
                themeProvider.toggleTheme();
              },
              activeColor: Theme.of(context).colorScheme.secondary,
            ),
            Text('Modo Oscuro: ${themeProvider.isDarkMode() ? 'Activado' : 'Desactivado'}'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: Icon(Icons.add),
      ),
    );
  }
}

Cambios clave en main.dart:

  1. Carga asíncrona: En main(), antes de runApp(), llamamos a ThemeProvider.loadTheme() para obtener la preferencia de tema guardada.
  2. ChangeNotifierProvider: Envolvemos MaterialApp con ChangeNotifierProvider<ThemeProvider>. Esto hace que una instancia de ThemeProvider esté disponible en el árbol de widgets. Utilizamos el themeProvider precargado.
  3. Consumer: Usamos Consumer<ThemeProvider> para reconstruir MaterialApp cuando el tema cambia, lo que aplica el nuevo ThemeData a toda la aplicación.
  4. Acceso al tema: Dentro de MyHomePage, usamos Provider.of<ThemeProvider>(context) para acceder a la instancia de ThemeProvider y obtener el tema actual (themeProvider.getTheme()) y el estado (themeProvider.isDarkMode()).
  5. Toggle de tema: Un IconButton en el AppBar y un Switch en el body llaman a themeProvider.toggleTheme() para cambiar el tema.

Paso 4: Probar la Aplicación ✅

Ejecuta tu aplicación en un emulador o dispositivo real:

flutter run

Deberías ver una interfaz de usuario con el tema claro por defecto (o el último guardado). Al pulsar el icono de sol/luna en la barra de aplicación o el Switch, el tema debería cambiar instantáneamente entre claro y oscuro. Si cierras la aplicación y la vuelves a abrir, el último tema seleccionado debería persistir.

App inicia Cargar preferencia de tema (Shared Prefs) Inicializar ThemeProvider Renderizar MaterialApp con ThemeProvider Usuario interactúa con botón de tema ThemeProvider.toggleTheme() Guardar nueva preferencia (Shared Prefs) notifyListeners() MaterialApp reconstruye con nuevo tema

Estructura del Proyecto Final 📂

Tu proyecto debería tener una estructura similar a esta:

flutter_dynamic_themes/
├── lib/
│   ├── main.dart
│   ├── theme_data.dart
│   └── theme_provider.dart
├── pubspec.yaml
└── ... (otros archivos de Flutter)

Mejores Prácticas y Consideraciones Adicionales 💡

  • Consistencia en los temas: Asegúrate de definir todos los colores y estilos importantes en ThemeData para que tu aplicación se vea consistente en ambos temas.
  • Widgets personalizados: Si tienes widgets personalizados que no utilizan los colores del tema de Flutter por defecto (por ejemplo, colores directamente codificados), asegúrate de adaptarlos para que respondan al cambio de tema.
  • Animaciones de transición: Para una experiencia de usuario más pulida, podrías considerar agregar animaciones al cambiar de tema. Esto se puede lograr con AnimatedTheme o creando tus propias transiciones. Sin embargo, esto añade complejidad y está fuera del alcance de este tutorial básico.
  • Testeo: Realiza pruebas exhaustivas en ambos modos (claro y oscuro) y en diferentes dispositivos para asegurarte de que todo se vea y funcione correctamente.
  • Más allá de claro/oscuro: El mismo patrón se puede extender para ofrecer múltiples temas (por ejemplo, verde, azul, etc.) simplemente añadiendo más ThemeData y lógica en tu ThemeProvider.

Tabla Comparativa: provider vs. setState

CaracterísticasetStateprovider
---------
AlcanceSolo el widget actual y sus hijos directos.Árbol de widgets completo, desde el punto de inyección.
ReconstrucciónReconstruye todo el widget.Reconstruye solo los widgets que 'escuchan' los cambios.
---------
ComplejidadSimple para estados locales pequeños.Requiere configuración inicial, pero escalable.
Recomendado paraEstados locales efímeros.Gestión de estado global o compartida.
---------
Legibilidad del códigoPuede volverse difícil de seguir en apps grandes.Mejora la separación de preocupaciones y la legibilidad.
¿Por qué `shared_preferences` y no otra solución de persistencia? shared_preferences es ideal para almacenar datos simples y no críticos como las preferencias del usuario (ajustes de tema, idioma, etc.). Es ligero y fácil de usar. Para datos más complejos o grandes volúmenes, se considerarían otras opciones como bases de datos (Hive, SQLite) o almacenamiento de archivos.

Conclusión 🎉

¡Felicidades! Has implementado con éxito temas dinámicos en tu aplicación Flutter utilizando provider para una gestión de estado reactiva y shared_preferences para la persistencia de las preferencias del usuario. Esta funcionalidad no solo mejora la estética de tu aplicación, sino que también ofrece una experiencia de usuario más moderna y personalizada.

Experimenta con diferentes combinaciones de colores en tus ThemeData para encontrar el estilo perfecto para tu aplicación. Recuerda que la clave es mantener la consistencia y la accesibilidad en ambos temas.


💡 Siguiente Paso: ¿Por qué no exploras cómo aplicar temas condicionalmente basándote en la hora del día o en la configuración del sistema operativo del usuario?

Tutoriales relacionados

Comentarios (0)

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