refactor(state): extract recording and search state, scope screen rebuilds
- New EstadoGrabacion owns the recording service, subscription, directory/size preferences and open-file actions - New EstadoBusqueda owns search, nearby stations, pagination and the min-bitrate filter - New orden_emisoras.dart with the OrdenEmisoras enum, shared sorter and list identity memoization so context.select comparisons work on derived lists - Large screens (inicio, buscar, favoritos, ajustes, reproductor) consume scoped selects/dedicated notifiers instead of root context.watch<EstadoRadio>, so audio buffer events no longer rebuild whole screens - Remove all 15 TODO(S4b) compat members from EstadoRadio; consumers use the dedicated providers. EstadoRadio drops from ~1121 to 753 lines, keeping playback/stations/favorites orchestration - 8 new tests including a rebuild-scoping probe (110 total green), flutter analyze clean
This commit is contained in:
@@ -9,6 +9,7 @@ import 'package:share_plus/share_plus.dart' show Share, XFile;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../estado/estado_ecualizador.dart';
|
||||
import '../estado/estado_grabacion.dart';
|
||||
import '../estado/estado_idioma.dart';
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../l10n/display_names.dart';
|
||||
@@ -85,7 +86,7 @@ class _SeccionGrabaciones extends StatelessWidget {
|
||||
const _SeccionGrabaciones();
|
||||
|
||||
Future<void> _seleccionarRuta(BuildContext context) async {
|
||||
final estado = context.read<EstadoRadio>();
|
||||
final estado = context.read<EstadoGrabacion>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final ruta = await FilePicker.platform.getDirectoryPath(
|
||||
@@ -93,7 +94,7 @@ class _SeccionGrabaciones extends StatelessWidget {
|
||||
);
|
||||
if (ruta == null) return;
|
||||
try {
|
||||
await estado.cambiarDirectorioGrabacion(ruta);
|
||||
await estado.cambiarDirectorio(ruta);
|
||||
if (!context.mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsPathUpdated)),
|
||||
@@ -107,10 +108,10 @@ class _SeccionGrabaciones extends StatelessWidget {
|
||||
}
|
||||
|
||||
Future<void> _restaurarRuta(BuildContext context) async {
|
||||
final estado = context.read<EstadoRadio>();
|
||||
final estado = context.read<EstadoGrabacion>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
await estado.restaurarDirectorioGrabacion();
|
||||
await estado.restaurarDirectorio();
|
||||
if (!context.mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsDefaultFolderRestored)),
|
||||
@@ -118,11 +119,11 @@ class _SeccionGrabaciones extends StatelessWidget {
|
||||
}
|
||||
|
||||
Future<void> _abrirCarpeta(BuildContext context) async {
|
||||
final estado = context.read<EstadoRadio>();
|
||||
final estado = context.read<EstadoGrabacion>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
try {
|
||||
final abierto = await estado.abrirDirectorioGrabacion();
|
||||
final abierto = await estado.abrirDirectorio();
|
||||
if (!context.mounted) return;
|
||||
if (!abierto) {
|
||||
messenger.showSnackBar(
|
||||
@@ -138,9 +139,9 @@ class _SeccionGrabaciones extends StatelessWidget {
|
||||
}
|
||||
|
||||
Future<void> _editarTamanoMaximo(BuildContext context) async {
|
||||
final estado = context.read<EstadoRadio>();
|
||||
final estado = context.read<EstadoGrabacion>();
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final actualMb = _bytesAMegabytes(estado.maxBytesGrabacion);
|
||||
final actualMb = _bytesAMegabytes(estado.maxBytes);
|
||||
final controller = TextEditingController(text: actualMb.toString());
|
||||
|
||||
final nuevoMb = await showModalBottomSheet<int>(
|
||||
@@ -186,7 +187,7 @@ class _SeccionGrabaciones extends StatelessWidget {
|
||||
);
|
||||
controller.dispose();
|
||||
if (nuevoMb == null || !context.mounted) return;
|
||||
await estado.cambiarMaxBytesGrabacion(nuevoMb * 1024 * 1024);
|
||||
await estado.cambiarMaxBytes(nuevoMb * 1024 * 1024);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.recordingsMaxSizeSaved(nuevoMb))),
|
||||
@@ -198,7 +199,9 @@ class _SeccionGrabaciones extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final estado = context.watch<EstadoRadio>();
|
||||
// Recording state lives in EstadoGrabacion (S4-R2): this section only
|
||||
// rebuilds on recording changes, never on playback notifications.
|
||||
final estado = context.watch<EstadoGrabacion>();
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return PluriGlassSurface(
|
||||
@@ -216,7 +219,7 @@ class _SeccionGrabaciones extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
FutureBuilder<String>(
|
||||
future: estado.directorioGrabacionEfectivo(),
|
||||
future: estado.directorioEfectivo(),
|
||||
builder:
|
||||
(ctx, snap) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@@ -256,9 +259,7 @@ class _SeccionGrabaciones extends StatelessWidget {
|
||||
leading: const Icon(Icons.sd_storage_rounded),
|
||||
title: Text(l10n.recordingsMaxSizeTitle),
|
||||
subtitle: Text(
|
||||
l10n.recordingsMaxSizeSubtitle(
|
||||
_bytesAMegabytes(estado.maxBytesGrabacion),
|
||||
),
|
||||
l10n.recordingsMaxSizeSubtitle(_bytesAMegabytes(estado.maxBytes)),
|
||||
),
|
||||
onTap: () => _editarTamanoMaximo(context),
|
||||
),
|
||||
@@ -301,8 +302,10 @@ class _SeccionTimerSueno extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
final estado = context.watch<EstadoRadio>();
|
||||
final presets = estado.timerSuenoPresetsSegundos;
|
||||
// S4-R5: scoped select — rebuilds only when the presets list changes.
|
||||
final presets = context.select<EstadoRadio, List<int>>(
|
||||
(e) => e.timerSuenoPresetsSegundos,
|
||||
);
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -654,7 +657,10 @@ class _SeccionOrdenListas extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final estado = context.watch<EstadoRadio>();
|
||||
// S4-R5: scoped select — rebuilds only when the ordering changes.
|
||||
final orden = context.select<EstadoRadio, OrdenEmisoras>(
|
||||
(e) => e.ordenListas,
|
||||
);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
@@ -684,9 +690,9 @@ class _SeccionOrdenListas extends StatelessWidget {
|
||||
label: Text(l10n.stationOrderByQuality),
|
||||
),
|
||||
],
|
||||
selected: {estado.ordenListas},
|
||||
selected: {orden},
|
||||
onSelectionChanged: (value) {
|
||||
estado.cambiarOrdenListas(value.first);
|
||||
context.read<EstadoRadio>().cambiarOrdenListas(value.first);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -790,9 +796,11 @@ class _SeccionGruposFavoritos extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final estado = context.watch<EstadoRadio>();
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final grupos = estado.gruposFavoritos;
|
||||
// S4-R5: scoped select — rebuilds only when the groups list changes.
|
||||
final grupos = context.select<EstadoRadio, List<GrupoFavoritos>>(
|
||||
(e) => e.gruposFavoritos,
|
||||
);
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -858,11 +866,18 @@ class _SeccionEmisoraPreferida extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final estado = context.watch<EstadoRadio>();
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final favoritas = estado.listaFavoritos;
|
||||
final preferida = estado.emisoraPreferida;
|
||||
final opciones = _opciones(estado, preferida);
|
||||
// S4-R5: scoped selects over identity-memoized getters.
|
||||
final favoritas = context.select<EstadoRadio, List<Emisora>>(
|
||||
(e) => e.listaFavoritos,
|
||||
);
|
||||
final disponibles = context.select<EstadoRadio, List<Emisora>>(
|
||||
(e) => e.emisorasDisponiblesPreferencia,
|
||||
);
|
||||
final preferida = context.select<EstadoRadio, Emisora?>(
|
||||
(e) => e.emisoraPreferida,
|
||||
);
|
||||
final opciones = _opciones(favoritas, disponibles, preferida);
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
@@ -947,11 +962,12 @@ class _SeccionEmisoraPreferida extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
List<Emisora> _opciones(EstadoRadio estado, Emisora? preferida) {
|
||||
final base =
|
||||
estado.listaFavoritos.isNotEmpty
|
||||
? estado.listaFavoritos
|
||||
: estado.emisorasDisponiblesPreferencia;
|
||||
List<Emisora> _opciones(
|
||||
List<Emisora> favoritas,
|
||||
List<Emisora> disponibles,
|
||||
Emisora? preferida,
|
||||
) {
|
||||
final base = favoritas.isNotEmpty ? favoritas : disponibles;
|
||||
final mapa = <String, Emisora>{
|
||||
for (final emisora in base) emisora.uuid: emisora,
|
||||
};
|
||||
@@ -967,8 +983,10 @@ class _SeccionEmisoras extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final estado = context.watch<EstadoRadio>();
|
||||
final custom = estado.emisorasCustom;
|
||||
// S4-R5: scoped select — rebuilds only when the custom list changes.
|
||||
final custom = context.select<EstadoRadio, List<Emisora>>(
|
||||
(e) => e.emisorasCustom,
|
||||
);
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
|
||||
Reference in New Issue
Block a user