Arquitectura de almacenamiento persistente: Datos clave-valor
Guarda los datos de la aplicación en el almacenamiento de clave-valor del dispositivo del usuario.
La mayoría de las aplicaciones Flutter, sin importar qué tan pequeñas o grandes sean, requieren almacenar datos en el dispositivo del usuario en algún momento, como claves de API, preferencias del usuario o datos que deberían estar disponibles sin conexión.
En esta receta, aprenderás a integrar el almacenamiento persistente para datos clave-valor en una aplicación Flutter que utiliza el diseño de arquitectura de Flutter recomendado. Si no estás familiarizado en absoluto con el almacenamiento de datos en disco, puedes leer la receta Almacenar datos clave-valor en el disco.
Los almacenes de clave-valor se utilizan a menudo para guardar datos simples, como la configuración de la app, y en esta receta los usarás para guardar las preferencias del Modo Oscuro. Si quieres aprender a almacenar datos complejos en un dispositivo, probablemente querrás usar SQL. En ese caso, echa un vistazo a la receta del cookbook que sigue a esta, llamada Arquitectura de almacenamiento persistente: SQL.
Aplicación de ejemplo: App con selección de tema
#La aplicación de ejemplo consiste en una sola pantalla con una barra de aplicación en la parte superior, una lista de elementos y una entrada de campo de texto en la parte inferior.
En el AppBar,
un Switch permite a los usuarios cambiar entre los modos de tema oscuro y claro.
Esta configuración se aplica inmediatamente y se almacena en el dispositivo
usando un servicio de almacenamiento de datos clave-valor.
La configuración se restaura cuando el usuario inicia la aplicación nuevamente.
Almacenar datos clave-valor de selección de tema
#Esta funcionalidad sigue el patrón de diseño de arquitectura de Flutter recomendado, con una capa de presentación y una de datos.
- La capa de presentación contiene el Widget
ThemeSwitchy elThemeSwitchViewModel. - La capa de datos contiene el
ThemeRepositoryy elSharedPreferencesService.
Capa de presentación de selección de tema
#El ThemeSwitch es un StatelessWidget que contiene un Widget Switch.
El State del switch está representado
por el campo público isDarkMode en el ThemeSwitchViewModel.
Cuando el usuario toca el switch,
el código ejecuta el comando toggle en el view model.
class ThemeSwitch extends StatelessWidget {
const ThemeSwitch({super.key, required this.viewmodel});
final ThemeSwitchViewModel viewmodel;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
const Text('Dark Mode'),
ListenableBuilder(
listenable: viewmodel,
builder: (context, _) {
return Switch(
value: viewmodel.isDarkMode,
onChanged: (_) {
viewmodel.toggle.execute();
},
);
},
),
],
),
);
}
}
El ThemeSwitchViewModel implementa un view model
como se describe en el patrón MVVM.
Este view model contiene el State del Widget ThemeSwitch,
representado por la variable booleana _isDarkMode.
El view model utiliza el ThemeRepository
para almacenar y cargar la configuración del modo oscuro.
Contiene dos acciones de comando diferentes:
load, que carga la configuración del modo oscuro desde el repositorio,
y toggle, que cambia el State entre modo oscuro y modo claro.
Expone el State a través del getter isDarkMode.
El método _load implementa el comando load.
Este método llama a ThemeRepository.isDarkMode
para obtener la configuración almacenada y llama a notifyListeners() para refrescar la UI.
El método _toggle implementa el comando toggle.
Este método llama a ThemeRepository.setDarkMode
para almacenar la nueva configuración del modo oscuro.
Asimismo, cambia el State local de _isDarkMode
y luego llama a notifyListeners() para actualizar la UI.
class ThemeSwitchViewModel extends ChangeNotifier {
ThemeSwitchViewModel(this._themeRepository) {
load = Command0(_load)..execute();
toggle = Command0(_toggle);
}
final ThemeRepository _themeRepository;
bool _isDarkMode = false;
/// If true show dark mode
bool get isDarkMode => _isDarkMode;
late final Command0<void> load;
late final Command0<void> toggle;
/// Load the current theme setting from the repository
Future<Result<void>> _load() async {
final result = await _themeRepository.isDarkMode();
if (result is Ok<bool>) {
_isDarkMode = result.value;
}
notifyListeners();
return result;
}
/// Toggle the theme setting
Future<Result<void>> _toggle() async {
_isDarkMode = !_isDarkMode;
final result = await _themeRepository.setDarkMode(_isDarkMode);
notifyListeners();
return result;
}
}
Capa de datos de selección de tema
#Siguiendo las pautas de arquitectura,
la capa de datos se divide en dos partes:
el ThemeRepository y el SharedPreferencesService.
El ThemeRepository es la única fuente de verdad
para todas las configuraciones de tematización,
y maneja cualquier posible error proveniente de la capa de servicio.
En este ejemplo,
el ThemeRepository también expone la configuración del modo oscuro
a través de un Stream observable.
Esto permite que otras partes de la aplicación
se suscriban a los cambios en la configuración del modo oscuro.
El ThemeRepository depende de SharedPreferencesService.
El repositorio obtiene el valor almacenado desde el servicio,
y lo almacena cuando cambia.
El método setDarkMode() pasa el nuevo valor al StreamController,
de modo que cualquier componente que escuche el stream observeDarkMode
class ThemeRepository {
ThemeRepository(this._service);
final _darkModeController = StreamController<bool>.broadcast();
final SharedPreferencesService _service;
/// Get if dark mode is enabled
Future<Result<bool>> isDarkMode() async {
try {
final value = await _service.isDarkMode();
return Result.ok(value);
} on Exception catch (e) {
return Result.error(e);
}
}
/// Set dark mode
Future<Result<void>> setDarkMode(bool value) async {
try {
await _service.setDarkMode(value);
_darkModeController.add(value);
return Result.ok(null);
} on Exception catch (e) {
return Result.error(e);
}
}
/// Stream that emits theme config changes.
/// ViewModels should call [isDarkMode] to get the current theme setting.
Stream<bool> observeDarkMode() => _darkModeController.stream;
}
El SharedPreferencesService envuelve
la funcionalidad del plugin SharedPreferences,
y realiza llamadas a los métodos setBool() y getBool()
para almacenar la configuración del modo oscuro,
ocultando esta dependencia de terceros del resto de la aplicación
class SharedPreferencesService {
static const String _kDarkMode = 'darkMode';
Future<void> setDarkMode(bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_kDarkMode, value);
}
Future<bool> isDarkMode() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_kDarkMode) ?? false;
}
}
Poniéndolo todo junto
#En este ejemplo,
el ThemeRepository y el SharedPreferencesService se crean
en el método main()
y se pasan al MainApp como dependencia de argumento del constructor.
void main() {
// ···
runApp(
MainApp(
themeRepository: ThemeRepository(SharedPreferencesService()),
// ···
),
);
}
Luego, cuando se crea el ThemeSwitch,
también se crea el ThemeSwitchViewModel
y se pasa el ThemeRepository como dependencia.
ThemeSwitch(
viewmodel: ThemeSwitchViewModel(widget.themeRepository),
),
La aplicación de ejemplo también incluye la clase MainAppViewModel,
la cual escucha los cambios en el ThemeRepository
y expone la configuración del modo oscuro al Widget MaterialApp.
class MainAppViewModel extends ChangeNotifier {
MainAppViewModel(this._themeRepository) {
_subscription = _themeRepository.observeDarkMode().listen((isDarkMode) {
_isDarkMode = isDarkMode;
notifyListeners();
});
_load();
}
final ThemeRepository _themeRepository;
StreamSubscription<bool>? _subscription;
bool _isDarkMode = false;
bool get isDarkMode => _isDarkMode;
Future<void> _load() async {
final result = await _themeRepository.isDarkMode();
if (result is Ok<bool>) {
_isDarkMode = result.value;
}
notifyListeners();
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
}
ListenableBuilder(
listenable: _viewModel,
builder: (context, child) {
return MaterialApp(
theme: _viewModel.isDarkMode ? ThemeData.dark() : ThemeData.light(),
home: child,
);
},
child: //...
)
A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-05. Ver código fuente oreportar un problema.