Persist the exploration, proposal, spec, design, tasks, and verify/archive reports produced during the multi-device EQ, alarm-countdown, and notification-visual-polish SDD cycles.
7.9 KiB
Design: EQ Device Auto-Switch UX
Technical Approach
Fix the startup device-seeding bug by adding a single obtenerDispositivoActual() call inside cargarPersistido() after the subscription is wired. Add a nombresDispositivos persistence layer (JSON map in SharedPreferences) exposed through EstadoEcualizador. Rewrite _SeccionEcualizadorAvanzado device rows with connection dot, display name fallback chain, and tap-to-edit bottom sheet containing rename TextField + embedded EcualizadorWidget.
Architecture Decisions
Decision: Bug fix location
| Option | Tradeoff | Decision |
|---|---|---|
A. Call obtenerDispositivoActual() in cargarPersistido() after _configurarSuscripcionDispositivo() |
Minimal change, Dart-only, no platform touch | Chosen |
B. Self-initializing stream (emit current device on onListen from Kotlin) |
Single code path for all events | Rejected: requires Kotlin change, out of scope |
| C. Persist last-known device ID to SP | Survives process kill | Rejected: stale-state risk, higher complexity |
Rationale: The obtenerDispositivoActual() method channel already exists and is tested on the platform side. One await call seeds _dispositivoActualId, triggers first-seen bootstrap, and re-resolves the preset. Zero platform code changes.
Decision: Device names persistence location
| Option | Tradeoff | Decision |
|---|---|---|
A. New SP key eq_nombres_dispositivos_v1 in ServicioEcualizador as Map<String, String> JSON |
Reuses existing _leerMapa/_guardarMapa pattern (adapted for String values), co-located with other EQ SP keys |
Chosen |
B. Field on DispositivoAudio model |
Requires model change + breaks value-equality contract (id-only) | Rejected |
C. Separate ServicioNombresDispositivo |
Over-engineered for a single map | Rejected |
Rationale: ServicioEcualizador already manages 6 SP keys with the same read/write JSON pattern. Adding a 7th key with a simpler Map<String, String> type keeps all EQ persistence in one service. The DispositivoAudio model stays pure (id-based equality, no persistence coupling).
Decision: Modal architecture
| Option | Tradeoff | Decision |
|---|---|---|
A. showModalBottomSheet with rename + embedded EcualizadorWidget |
Established pattern (used 4x in settings), isScrollControlled: true handles tall content |
Chosen |
| B. Inline expanded accordion per device | Always visible, but very tall list, complex expand/collapse state | Rejected |
| C. Full-screen dialog / page route | Overkill for rename + 5-band slider | Rejected |
Rationale: The app already uses showModalBottomSheet with showDragHandle: true and isScrollControlled: true in _editarGrupo, _editarTamanoMaximo, _FormularioDuracionTimer, and _mostrarFormularioAnadir. Same pattern, consistent UX.
Decision: Display name resolution
| Option | Tradeoff | Decision |
|---|---|---|
A. Fallback chain: nombresDispositivos[id] > DispositivoAudio.nombre > id |
Clean, handles all null/empty cases, no model changes | Chosen |
| B. Merge custom name into a new model field | Couples persistence to model | Rejected |
Rationale: The display name is a pure UI concern. DispositivoAudio.nombre comes from platform (productName) and can be null/empty on some devices. The fallback chain is computed at render time in the settings widget, keeping the model and state layers clean.
Decision: Connection status derivation
Choice: Compare entry.key == eq.dispositivoActualId per row to show a green dot. No new API needed -- EstadoEcualizador.dispositivoActualId getter already exists.
Rejected: Tracking a Set<String> connectedDevices from the stream -- unnecessary since only one audio output is active at a time on Android.
Data Flow
App startup
|
v
cargarPersistido()
|-- servicio.cargar() --> loads config + nombresDispositivos
|-- _configurarSuscripcionDispositivo() --> subscribes to EventChannel
|-- obtenerDispositivoActual() --> seeds _dispositivoActualId [NEW]
|-- _onDispositivoCambiado() --> first-seen bootstrap + re-resolve
|-- audio.aplicarPreset() --> correct preset applied
v
Settings UI (tap device row)
|
v
showModalBottomSheet
|-- TextField (custom name) --> renombrarDispositivo(id, name)
| |-> servicio.guardarNombresDispositivos(map)
| |-> notifyListeners()
|
|-- EcualizadorWidget(preset, onCambio)
|-> eq.cambiarPresetDispositivo(id, preset) [NEW convenience method]
File Changes
| File | Action | Description |
|---|---|---|
lib/estado/estado_ecualizador.dart |
Modify | Add obtenerDispositivoActual() call in cargarPersistido() after subscription setup. Add _nombresDispositivos map, renombrarDispositivo(), obtenerNombreDispositivo(), nombreVisible() fallback helper. |
lib/servicios/servicio_ecualizador.dart |
Modify | Add _keyNombresDispositivos = 'eq_nombres_dispositivos_v1'. Add cargarNombresDispositivos(), guardarNombresDispositivos() methods. Load names inside cargar() and include in ConfiguracionEcualizador. |
lib/servicios/servicio_ecualizador.dart (ConfiguracionEcualizador) |
Modify | Add nombresDispositivos field (Map<String, String>). |
lib/pantallas/pantalla_ajustes.dart |
Modify | Rewrite _SeccionEcualizadorAvanzado device rows: add connection dot (green CircleAvatar 8px), display name via fallback chain, onTap → _mostrarEdicionDispositivo(). Add new _DialogoEdicionDispositivo StatefulWidget (bottom sheet body: TextField for name + EcualizadorWidget). |
lib/l10n/app_en.arb |
Modify | Add ~5 new keys: advancedEqDeviceConnected, advancedEqDeviceRenameLabel, advancedEqDeviceEditTitle, advancedEqDeviceNameHint, advancedEqDeviceRenamed. |
lib/l10n/app_*.arb (12 files) |
Modify | Translate the new keys to all supported locales. |
test/ |
Create | Tests for: startup device seeding, rename persistence round-trip, display name fallback chain, connection indicator logic. |
Interfaces / Contracts
// ConfiguracionEcualizador -- add field
class ConfiguracionEcualizador {
// ... existing fields ...
final Map<String, String> nombresDispositivos; // deviceId -> custom name
}
// EstadoEcualizador -- new public API
Map<String, String> get nombresDispositivos;
Future<void> renombrarDispositivo(String deviceId, String nombre);
String obtenerNombreDispositivo(String deviceId); // returns custom name or ''
String nombreVisible(String deviceId, String platformName); // fallback chain
// ServicioEcualizador -- new methods
Future<Map<String, String>> cargarNombresDispositivos();
Future<void> guardarNombresDispositivos(Map<String, String> nombres);
Testing Strategy
| Layer | What to Test | Approach |
|---|---|---|
| Unit | cargarPersistido() seeds _dispositivoActualId when multi-device ON |
Fake ServicioDispositivoAudio returns known device; assert dispositivoActualId != null after load |
| Unit | renombrarDispositivo persists and notifies |
Call rename, verify SP key written and listeners notified |
| Unit | nombreVisible fallback chain: custom > platform > id |
Three cases: all present, no custom, no platform |
| Unit | Connection indicator: entry.key == dispositivoActualId |
Verify boolean derivation in widget test or pure logic |
| Widget | Bottom sheet renders rename + EQ | pumpWidget with _DialogoEdicionDispositivo, verify TextField and EcualizadorWidget present |
Migration / Rollout
No migration required. The new SP key eq_nombres_dispositivos_v1 is created on first rename. If the code is reverted, the key is simply ignored -- no data loss or corruption. The startup fix is additive (one extra method call).
Open Questions
- None blocking. The
nombresDispositivosomission from export schema v4 is acknowledged and deferred per proposal.