Files
pluriwave/lib/servicios/servicio_alarmas.dart
T
FreeTLab 80538900db fix(alarmas,auto): guard the last unguarded snooze path, surface car progress
Continuation of 7054a4c: the native anchor guard alone did not fix the
reported ~1444-minute snooze, because Dart runs AFTERWARDS on the
pre-notice path and had no guard at all.

1. Snooze from the pre-notice notification, root cause.

app.dart dispatches AFTER the receiver's postponeNext already ran and
after startActivity, and EstadoAlarmas.posponerProximaDesdePreaviso took
whatever occurrence it was handed on faith, then persisted and
rescheduled from it -- the last snooze path in the codebase with no
occurrence guard. The occurrence itself is not trustworthy either:
app.dart falls back to alarma.proximaEjecucion when the native event
carries none, and that field can already point at tomorrow.

_ocurrenciaSonando is generalized into _ocurrenciaValida with a caller-
supplied forward allowance and an externally-proposed occurrence that
still has to survive the same check. The pre-notice path gets a
ventanaPreaviso (30 min, matching AlarmScheduler.PRE_NOTICE_MILLIS) --
unlike the ringing-screen guard, this occurrence legitimately has not
happened yet, which is exactly why the existing helper could not just be
reused here.

Also heals state already poisoned by the missing guard: a snoozeHasta
parked past a 3-hour ceiling (posponerEjecucion clamps to 120 minutes,
so anything beyond that is corruption, not a long real snooze) is
dropped on recalculation. Without it, an alarm poisoned on a build
before this fix keeps reporting tomorrow after updating, and the user
reasonably concludes nothing changed.

2. Android Auto: no progress bar or time labels on a local track.

updatePosition was never set anywhere in the handler, so it sat at its
Duration.zero default while copyWith refreshed updateTime to now on every
push -- the car was told "position 0, as of right now" on every event, a
bar pinned at the start regardless of what was actually playing. Now set
from _player.position on both the player-state and buffered-position
listeners (the latter ticks ~2/s, which is what keeps the car's bar
smooth between player-state events). Also stream the MediaItem's
duration once the source reports it -- Auto draws no bar at all without
one, and radio streams correctly keep reporting none (live audio has no
length).

3. Android Auto: drop the Ecualizador browsable folder.

Owner decision after driving with it: a browsable six-preset list is
more interaction than a driver wants, and on/off from all three player
views (already fixed in 7054a4c to win the custom-action slot) is the
only equalizer control that belongs in the car. Preset selection stays on
the phone. This lands back on the redesign mockup's original rule ("sin
carpeta de ecualizador"), now for a road-tested reason. getChildren keeps
answering the folder's id transitionally, since a head unit can have the
old tree cached for a session or two.

The two "raiz always includes/ends with Ecualizador" tests are replaced,
not regressed -- same move the codebase already made once in the other
direction for the same folder.

Tests: 1127 -> 1132.
2026-08-05 23:07:10 +02:00

634 lines
23 KiB
Dart

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
import '../modelos/alarma_musical.dart';
import '../modelos/emisora.dart';
import 'persistencia_tolerante.dart';
import 'servicio_programacion_alarmas.dart';
class ConfiguracionAlarmas {
const ConfiguracionAlarmas({
required this.alarmas,
required this.vacaciones,
required this.excepciones,
});
final List<AlarmaMusical> alarmas;
final List<RangoVacaciones> vacaciones;
final List<ExcepcionAlarma> excepciones;
}
class ServicioAlarmas {
ServicioAlarmas({
ServicioProgramacionAlarmas? programacion,
SharedPreferences? prefs,
DateTime Function()? reloj,
}) : _programacion = programacion ?? ServicioProgramacionAlarmas(),
_prefs = prefs,
_reloj = reloj ?? DateTime.now;
static const _keyConfig = 'alarmas_musicales_v1';
final ServicioProgramacionAlarmas _programacion;
final SharedPreferences? _prefs;
final DateTime Function() _reloj;
final _uuid = const Uuid();
/// Current time through the injected clock, so callers that need "now"
/// (e.g. the ring screen's snooze anchor) stay testable with the same
/// fixture clock the service itself computes with.
DateTime ahora() => _reloj();
// In-memory cache + single-writer queue (Design 3.5 / S3-R7): every
// mutation runs serialized through [_enCola] and reads [_cache], killing
// the read-modify-write race the old cargar()-before-each-mutation had.
ConfiguracionAlarmas? _cache;
String? _cacheRaw;
Future<void> _cola = Future<void>.value();
// persistence-resilience (D4): set when the top-level payload could not
// be decoded at all (vs. an individual entry inside it). While true, the
// AUTOMATIC writer (recalcularTodas) must never reach disk -- only an
// explicit user mutation (which always funnels through _guardar) may
// overwrite a payload we could not read. Cleared by _guardar and by the
// next successful (even if partial) read.
bool _lecturaAlarmasDegradada = false;
Future<T> _enCola<T>(Future<T> Function() accion) {
final resultado = _cola.then((_) => accion());
_cola = resultado.then((_) {}, onError: (_) {});
return resultado;
}
/// Re-reads from persistent storage (refreshing the cache) so writes done
/// outside this service — e.g. a settings import that rewrites the raw
/// key — are always picked up.
Future<ConfiguracionAlarmas> cargar() => _enCola(() {
_cache = null;
_cacheRaw = null;
return _configActual();
});
Future<ConfiguracionAlarmas> _configActual() async {
final existente = _cache;
if (existente != null) return existente;
final prefs = await _resolverPrefs();
final raw = prefs.getString(_keyConfig);
final config = _parsear(raw);
_cache = config;
return config;
}
/// Parses the persisted [raw] payload with per-entry tolerance
/// (persistence-resilience D1/D2) and updates [_cacheRaw] /
/// [_lecturaAlarmasDegradada] as a side effect, since each outcome needs
/// a DIFFERENT cached-raw value:
/// - top-level decode failure (bad JSON, or any container-level field of
/// the wrong shape) -> DEGRADED: empty config, [_cacheRaw] keeps the
/// corrupt [raw] untouched, [_lecturaAlarmasDegradada] set so
/// `recalcularTodas` never overwrites it (D4).
/// - decodes, but one or more entries are malformed -> PARTIAL: only the
/// survivors are kept, [_cacheRaw] is normalized to their own
/// serialization -- NOT the corrupt raw (D3) -- so the automatic
/// writer's dirty-guard compares against a coherent baseline instead of
/// re-firing on every 60s tick.
/// - fully healthy payload -> unchanged behavior; clears the flag if a
/// previous read had set it (suppression lifts on next successful
/// read).
ConfiguracionAlarmas _parsear(String? raw) {
if (raw == null || raw.trim().isEmpty) {
_lecturaAlarmasDegradada = false;
_cacheRaw = raw;
return const ConfiguracionAlarmas(
alarmas: [],
vacaciones: [],
excepciones: [],
);
}
try {
final data = jsonDecode(raw) as Map<String, dynamic>;
final alarmas = parseListaTolerante<AlarmaMusical>(
data['alarmas'] as List?,
AlarmaMusical.fromJson,
subsistema: 'alarmas',
coleccion: 'alarmas',
);
final vacaciones = parseListaTolerante<RangoVacaciones>(
data['vacaciones'] as List?,
RangoVacaciones.fromJson,
subsistema: 'alarmas',
coleccion: 'vacaciones',
);
final excepciones = parseListaTolerante<ExcepcionAlarma>(
data['excepciones'] as List?,
ExcepcionAlarma.fromJson,
subsistema: 'alarmas',
coleccion: 'excepciones',
);
final config = ConfiguracionAlarmas(
alarmas: alarmas.validas,
vacaciones: vacaciones.validas,
excepciones: excepciones.validas,
);
_lecturaAlarmasDegradada = false;
final huboSaltos =
alarmas.saltadas > 0 ||
vacaciones.saltadas > 0 ||
excepciones.saltadas > 0;
_cacheRaw = huboSaltos ? _serializar(config) : raw;
return config;
} catch (e) {
_lecturaAlarmasDegradada = true;
_cacheRaw = raw;
registrarSaltoPersistencia(
subsistema: 'alarmas',
detalle: _keyConfig,
razon: e.toString(),
);
return const ConfiguracionAlarmas(
alarmas: [],
vacaciones: [],
excepciones: [],
);
}
}
Future<ConfiguracionAlarmas> guardarAlarma(
AlarmaMusical alarma, {
List<RangoVacaciones>? vacaciones,
List<ExcepcionAlarma>? excepciones,
}) => _enCola(() async {
final config = await _configActual();
final ahora = _reloj();
final vacacionesActuales = vacaciones ?? config.vacaciones;
final excepcionesActuales = excepciones ?? config.excepciones;
final alarmas = List<AlarmaMusical>.from(config.alarmas);
final index = alarmas.indexWhere((a) => a.id == alarma.id);
final actualizada = alarma.copyWith(
creadaEn: alarma.creadaEn ?? ahora,
actualizadaEn: ahora,
);
if (index >= 0) {
alarmas[index] = actualizada;
} else {
alarmas.add(actualizada);
}
// Recalculate every alarm, not just the one just saved: siblings can
// hold a proximaEjecucion snapshot from the last periodic tick that is
// stale (even past-due), which would otherwise outrank a freshly
// created/activated/edited alarm when EstadoAlarmas.proximaAlarma sorts
// by proximaProgramable.
final nuevo = ConfiguracionAlarmas(
alarmas: _recalcularLista(
alarmas,
vacacionesActuales,
excepcionesActuales,
),
vacaciones: vacacionesActuales,
excepciones: excepcionesActuales,
);
await _guardar(nuevo);
return nuevo;
});
Future<ConfiguracionAlarmas> eliminarAlarma(String id) => _enCola(() async {
final config = await _configActual();
final restantes = config.alarmas.where((a) => a.id != id).toList();
final excepciones =
config.excepciones.where((e) => e.alarmaId != id).toList();
final nuevo = ConfiguracionAlarmas(
alarmas: _recalcularLista(restantes, config.vacaciones, excepciones),
vacaciones: config.vacaciones,
excepciones: excepciones,
);
await _guardar(nuevo);
return nuevo;
});
Future<ConfiguracionAlarmas> guardarVacaciones(
List<RangoVacaciones> vacaciones,
) => _enCola(() async {
final config = await _configActual();
final normalizadas =
vacaciones.map((v) => v.normalizado()).toList()
..sort((a, b) => a.inicioDia.compareTo(b.inicioDia));
final nuevo = ConfiguracionAlarmas(
alarmas: _recalcularLista(
config.alarmas,
normalizadas,
config.excepciones,
),
vacaciones: normalizadas,
excepciones: config.excepciones,
);
await _guardar(nuevo);
return nuevo;
});
RangoVacaciones crearRangoVacaciones({
required DateTime inicio,
required DateTime fin,
String? nombre,
}) {
final rango = RangoVacaciones(
id: _uuid.v4(),
nombre:
(nombre == null || nombre.trim().isEmpty)
? 'Vacaciones'
: nombre.trim(),
inicio: inicio,
fin: fin,
);
return rango.normalizado();
}
Future<ConfiguracionAlarmas> recalcularTodas() => _enCola(() async {
final config = await _configActual();
// persistence-resilience (D4): a degraded top-level read must never
// let this AUTOMATIC writer reach disk -- only an explicit user
// mutation (guardarAlarma/eliminarAlarma/etc., via _guardar) may
// persist over a payload we could not read.
if (_lecturaAlarmasDegradada) return config;
final alarmas = _recalcularLista(
config.alarmas,
config.vacaciones,
config.excepciones,
);
final nuevo = ConfiguracionAlarmas(
alarmas: alarmas,
vacaciones: config.vacaciones,
excepciones: config.excepciones,
);
// Dirty-guard (S3-R5): this runs every minute from the refresh timer;
// skip the SharedPreferences write when nothing actually changed.
final nuevoRaw = _serializar(nuevo);
final actualRaw = _cacheRaw ?? _serializar(config);
if (nuevoRaw == actualRaw) return config;
await _guardar(nuevo, raw: nuevoRaw);
return nuevo;
});
Future<ConfiguracionAlarmas> sincronizarEjecucionesNativas(
Map<String, DateTime> ejecuciones,
) {
if (ejecuciones.isEmpty) return cargar();
return _enCola(() => _sincronizarEjecucionesNativasInterno(ejecuciones));
}
Future<ConfiguracionAlarmas> _sincronizarEjecucionesNativasInterno(
Map<String, DateTime> ejecuciones,
) async {
final config = await _configActual();
final ahora = _reloj();
var huboCambios = false;
final alarmas =
config.alarmas.map((alarma) {
final gestionadaEn = ejecuciones[alarma.id];
if (gestionadaEn == null) return alarma;
final ultima = alarma.ultimaEjecucionGestionada;
if (ultima != null && !gestionadaEn.isAfter(ultima)) return alarma;
final proxima = alarma.proximaProgramable;
if (proxima != null &&
proxima.isAfter(
gestionadaEn.add(
ServicioProgramacionAlarmas.toleranciaDisparoInminente,
),
)) {
return alarma;
}
final siguiente = _programacion.calcularSiguienteDespuesDeEjecucion(
alarma: alarma,
ejecucion: gestionadaEn,
vacaciones: config.vacaciones,
excepciones: config.excepciones,
);
huboCambios = true;
return alarma.copyWith(
activa:
alarma.tipoProgramacion == TipoProgramacionAlarma.unica
? false
: alarma.activa,
proximaEjecucion: siguiente,
limpiarProximaEjecucion: true,
limpiarSnooze: true,
ultimaEjecucionGestionada: gestionadaEn,
actualizadaEn: ahora,
);
}).toList();
if (!huboCambios) return config;
final nuevo = ConfiguracionAlarmas(
alarmas: _recalcularLista(alarmas, config.vacaciones, config.excepciones),
vacaciones: config.vacaciones,
excepciones: config.excepciones,
);
await _guardar(nuevo);
return nuevo;
}
Future<ConfiguracionAlarmas> saltarProxima(
String alarmaId,
) => _enCola(() async {
final config = await _configActual();
final alarma = config.alarmas.firstWhere((a) => a.id == alarmaId);
final proxima = alarma.proximaEjecucion;
if (proxima == null) return config;
final excepciones = [
...config.excepciones,
ExcepcionAlarma(alarmaId: alarmaId, ejecucion: proxima, tipo: 'skipNext'),
];
final nuevo = ConfiguracionAlarmas(
alarmas: _recalcularLista(config.alarmas, config.vacaciones, excepciones),
vacaciones: config.vacaciones,
excepciones: excepciones,
);
await _guardar(nuevo);
return nuevo;
});
/// Records a scheduling-reliability failure for [alarmaId] (fix/alarmas-
/// fallos-silenciosos): reuses the SAME `ExcepcionAlarma` model
/// `saltarProxima` already persists, so `EstadoAlarmas.ultimaExcepcionPara`
/// surfaces it on the exact alarm card affected instead of only a
/// transient, alarm-agnostic message. A previous FAILURE record for the
/// same alarm is replaced (only the latest attempt's outcome matters) --
/// any `skipNext` exception for this or other alarms is left untouched.
/// Never affects scheduling: `ServicioProgramacionAlarmas._esValida` only
/// treats `tipoSaltoSiguiente` as an actual skip.
Future<ConfiguracionAlarmas> registrarFalloProgramacion(
String alarmaId,
DateTime ejecucion,
String tipo,
) => _enCola(() async {
final config = await _configActual();
final excepciones = [
..._sinFalloPrevio(config.excepciones, alarmaId),
ExcepcionAlarma(alarmaId: alarmaId, ejecucion: ejecucion, tipo: tipo),
];
final nuevo = ConfiguracionAlarmas(
alarmas: config.alarmas,
vacaciones: config.vacaciones,
excepciones: excepciones,
);
await _guardar(nuevo);
return nuevo;
});
/// Clears the outstanding failure record for [alarmaId] ONLY when its
/// current tipo is [tipo] (a subsequent attempt of THAT SPECIFIC kind
/// succeeded). Type-scoped on purpose: a successful main-alarm schedule
/// call proves nothing about the pre-notice or foreground-service
/// subsystems, so it must never clear a failure recorded for those. No-op
/// when there is nothing to clear or the recorded tipo does not match.
Future<ConfiguracionAlarmas> limpiarFalloProgramacion(
String alarmaId,
String tipo,
) => _enCola(() async {
final config = await _configActual();
final actual = config.excepciones.where((e) => e.alarmaId == alarmaId);
final tieneEseTipo = actual.any((e) => e.tipo == tipo);
if (!tieneEseTipo) return config;
final excepciones =
config.excepciones
.where((e) => !(e.alarmaId == alarmaId && e.tipo == tipo))
.toList();
final nuevo = ConfiguracionAlarmas(
alarmas: config.alarmas,
vacaciones: config.vacaciones,
excepciones: excepciones,
);
await _guardar(nuevo);
return nuevo;
});
List<ExcepcionAlarma> _sinFalloPrevio(
List<ExcepcionAlarma> excepciones,
String alarmaId,
) =>
excepciones
.where(
(e) =>
!(e.alarmaId == alarmaId &&
ExcepcionAlarma.tiposFallo.contains(e.tipo)),
)
.toList();
Future<ConfiguracionAlarmas> posponerEjecucion(
String alarmaId,
DateTime ejecucion,
int minutos,
) async {
// Unified snooze anchor (Design 2.2): occurrence + minutes, clamped to
// now + minutes when the target already passed. Matches the native
// AlarmScheduler.snooze/postponeNext semantics so both layers always
// land on the same re-fire time.
final seguros = minutos.clamp(1, 120);
final objetivo = ejecucion.add(Duration(minutes: seguros));
final ahora = _reloj();
final snoozeHasta =
objetivo.isAfter(ahora)
? objetivo
: ahora.add(Duration(minutes: seguros));
return posponerEjecucionHasta(alarmaId, ejecucion, snoozeHasta);
}
Future<ConfiguracionAlarmas> posponerEjecucionHasta(
String alarmaId,
DateTime ejecucion,
DateTime snoozeHasta,
) => _enCola(() async {
final config = await _configActual();
final ahora = _reloj();
final alarmas =
config.alarmas
.map(
(a) =>
a.id == alarmaId
? a.copyWith(
snoozeHasta: snoozeHasta,
snoozeOrigen: ejecucion,
ultimaEjecucionGestionada: ejecucion,
actualizadaEn: ahora,
)
: a,
)
.toList();
final nuevo = ConfiguracionAlarmas(
alarmas: _recalcularLista(alarmas, config.vacaciones, config.excepciones),
vacaciones: config.vacaciones,
excepciones: config.excepciones,
);
await _guardar(nuevo);
return nuevo;
});
Future<ConfiguracionAlarmas> completarEjecucion(
String alarmaId,
DateTime ejecucion,
) => _enCola(() async {
final config = await _configActual();
final ahora = _reloj();
final alarmas =
config.alarmas.map((a) {
if (a.id != alarmaId) return a;
final siguiente = _programacion.calcularSiguienteDespuesDeEjecucion(
alarma: a,
ejecucion: ejecucion,
vacaciones: config.vacaciones,
excepciones: config.excepciones,
);
return a.copyWith(
activa:
a.tipoProgramacion == TipoProgramacionAlarma.unica
? false
: a.activa,
proximaEjecucion: siguiente,
limpiarProximaEjecucion: true,
limpiarSnooze: true,
ultimaEjecucionGestionada: ejecucion,
actualizadaEn: ahora,
);
}).toList();
final nuevo = ConfiguracionAlarmas(
alarmas: _recalcularLista(alarmas, config.vacaciones, config.excepciones),
vacaciones: config.vacaciones,
excepciones: config.excepciones,
);
await _guardar(nuevo);
return nuevo;
});
AlarmaMusical crearAlarma({
required String nombre,
required int hora,
required int minuto,
required TipoProgramacionAlarma tipoProgramacion,
required List<int> diasSemana,
DateTime? fechaUnica,
Emisora? emisora,
Emisora? emisoraFallback,
bool sonarEnVacaciones = true,
int snoozeMinutos = 5,
double volumen = 0.85,
SonidoInternoAlarma sonidoInterno = SonidoInternoAlarma.amanecer,
}) {
final ahora = _reloj();
return AlarmaMusical(
id: _uuid.v4(),
nombre: nombre,
hora: hora,
minuto: minuto,
tipoProgramacion: tipoProgramacion,
diasSemana: diasSemana,
fechaUnica: fechaUnica,
emisora: emisora,
emisoraFallback: emisoraFallback,
sonarEnVacaciones: sonarEnVacaciones,
snoozeMinutos: snoozeMinutos,
volumen: volumen,
sonidoInterno: sonidoInterno,
creadaEn: ahora,
actualizadaEn: ahora,
);
}
/// Persists [config] and refreshes the in-memory cache (the cache is
/// "invalidated" by replacing it with the just-written state).
Future<void> _guardar(ConfiguracionAlarmas config, {String? raw}) async {
final serializado = raw ?? _serializar(config);
final prefs = await _resolverPrefs();
await prefs.setString(_keyConfig, serializado);
_cache = config;
_cacheRaw = serializado;
// persistence-resilience (D4): every explicit mutation funnels through
// here, so this is where write authority is restored after a degraded
// read -- user intent wins over a suppressed automatic writer.
_lecturaAlarmasDegradada = false;
}
String _serializar(ConfiguracionAlarmas config) => jsonEncode({
'alarmas': config.alarmas.map((a) => a.toJson()).toList(),
'vacaciones': config.vacaciones.map((v) => v.toJson()).toList(),
'excepciones': config.excepciones.map((e) => e.toJson()).toList(),
});
AlarmaMusical _recalcular(
AlarmaMusical alarma,
List<RangoVacaciones> vacaciones,
List<ExcepcionAlarma> excepciones,
) {
final ahora = _reloj();
// S2-R5: a disabled alarm must not keep a pending snooze; clearing it
// here guarantees the snoozed occurrence dies with the alarm.
// Self-heal for a snooze target parked absurdly far out — the reported
// "posponer left it 1400+ minutes away". A legitimate snooze can never
// reach here: posponerEjecucion clamps to `minutos.clamp(1, 120)` and the
// anchor is now guarded on both the native and Dart sides, so anything
// past that ceiling is a leftover from a build that had neither guard.
// Without this, an alarm poisoned before the fix keeps showing tomorrow
// on every tick — the user reinstalls, sees no change, and reasonably
// concludes nothing was fixed. Generous margin over the 120-minute cap so
// a real long snooze is never mistaken for corruption.
const techoSnooze = Duration(hours: 3);
final snoozeCorrupto =
alarma.snoozeHasta != null &&
alarma.snoozeHasta!.isAfter(ahora.add(techoSnooze));
final snoozeActivo =
alarma.activa &&
!snoozeCorrupto &&
alarma.snoozeHasta != null &&
alarma.snoozeHasta!.isAfter(ahora);
// Self-heal for state poisoned before the Detener anchor fix: a stop
// that closed a FUTURE occurrence wrote it into
// ultimaEjecucionGestionada, and _esValida rejects any candidate
// matching it -- so the alarm silently skips that day forever after,
// with nothing in the UI to explain it. An occurrence cannot have been
// handled before it happens, so a value meaningfully in the future is
// corrupt by definition and safe to drop: it can only ever suppress a
// real future ring, never prevent a double-fire (which needs a PAST
// occurrence to guard). Placed here, in the recalculation every load and
// every mutation already funnels through, so an affected alarm heals on
// the next app open with no user action.
final gestionada = alarma.ultimaEjecucionGestionada;
final gestionadaCorrupta =
gestionada != null &&
gestionada.isAfter(
ahora.add(ServicioProgramacionAlarmas.toleranciaDisparoInminente),
);
final saneada =
gestionadaCorrupta
? alarma.copyWith(limpiarUltimaEjecucionGestionada: true)
: alarma;
final proxima = _programacion.calcularProxima(
alarma: saneada,
desde: ahora,
vacaciones: vacaciones,
excepciones: excepciones,
);
return saneada.copyWith(
proximaEjecucion: proxima,
limpiarProximaEjecucion: true,
limpiarSnooze: !snoozeActivo,
);
}
// Every mutation that can shift "which alarm fires next" (create, edit,
// activate/deactivate, delete, fire, snooze, skip) recalculates the whole
// list, not just the alarm it touched directly. Siblings otherwise keep a
// proximaEjecucion snapshot from the last periodic tick, which can be
// stale (or already past-due) and wrongly outrank a freshly updated alarm
// wherever proximaProgramable values are compared (EstadoAlarmas.proximaAlarma).
List<AlarmaMusical> _recalcularLista(
List<AlarmaMusical> alarmas,
List<RangoVacaciones> vacaciones,
List<ExcepcionAlarma> excepciones,
) => alarmas.map((a) => _recalcular(a, vacaciones, excepciones)).toList();
Future<SharedPreferences> _resolverPrefs() async =>
_prefs ?? SharedPreferences.getInstance();
}