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.
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.
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.
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 elThemeDataactual (lightThemeodarkTheme)._isDarkMode: Unboolque indica si el tema actual es oscuro. Esto es crucial parashared_preferences.toggleTheme(): Cambia el_themeDatay_isDarkMode, guarda la preferencia y llama anotifyListeners()para reconstruir los widgets que escuchan.loadTheme(): Un método estático asíncrono que recupera la preferencia del tema guardada enSharedPreferencesy devuelve una instancia deThemeProviderinicializada con ese tema._saveThemePreference(): Guarda la preferencia del tema (truepara oscuro,falsepara claro) enSharedPreferences.
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:
- Carga asíncrona: En
main(), antes derunApp(), llamamos aThemeProvider.loadTheme()para obtener la preferencia de tema guardada. ChangeNotifierProvider: EnvolvemosMaterialAppconChangeNotifierProvider<ThemeProvider>. Esto hace que una instancia deThemeProvideresté disponible en el árbol de widgets. Utilizamos elthemeProviderprecargado.Consumer: UsamosConsumer<ThemeProvider>para reconstruirMaterialAppcuando el tema cambia, lo que aplica el nuevoThemeDataa toda la aplicación.- Acceso al tema: Dentro de
MyHomePage, usamosProvider.of<ThemeProvider>(context)para acceder a la instancia deThemeProvidery obtener el tema actual (themeProvider.getTheme()) y el estado (themeProvider.isDarkMode()). - Toggle de tema: Un
IconButtonen elAppBary unSwitchen elbodyllaman athemeProvider.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.
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
ThemeDatapara 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
AnimatedThemeo 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
ThemeDatay lógica en tuThemeProvider.
Tabla Comparativa: provider vs. setState
| Característica | setState | provider |
|---|---|---|
| --- | --- | --- |
| Alcance | Solo el widget actual y sus hijos directos. | Árbol de widgets completo, desde el punto de inyección. |
| Reconstrucción | Reconstruye todo el widget. | Reconstruye solo los widgets que 'escuchan' los cambios. |
| --- | --- | --- |
| Complejidad | Simple para estados locales pequeños. | Requiere configuración inicial, pero escalable. |
| Recomendado para | Estados locales efímeros. | Gestión de estado global o compartida. |
| --- | --- | --- |
| Legibilidad del código | Puede 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.
Tutoriales relacionados
- Flutter para Principiantes: Construyendo tu Primera Aplicación Interfaz de Usuario Moderna con Widgetsbeginner20 min
- Navegación Avanzada en Flutter: Rutas Dinámicas y Deep Linking con GoRouterintermediate18 min
- Flutter al Detalle: Animaciones Implícitas y Explícitas para Interfaces de Usuario Fluidas y Atractivasintermediate15 min
- Flutter para Principiantes: Construyendo una Lista de Tareas Interactivas y Persistentes con Isar DBbeginner30 min
- Flutter Avanzado: Sincronización de Datos Offline con WorkManager y Hive para Apps Robuadvanced20 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!