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:
2026-06-11 21:43:18 +02:00
parent 0416b301b2
commit 52855e75c2
17 changed files with 1195 additions and 643 deletions
+51 -40
View File
@@ -19,9 +19,15 @@ class PantallaFavoritos extends StatelessWidget {
@override
Widget build(BuildContext context) {
final estado = context.watch<EstadoRadio>();
final favoritos = estado.listaFavoritos;
final grupos = estado.gruposFavoritos;
// S4-R5: no root watch — select only the fields this screen reads. The
// getters are identity-memoized, so playback notifications that do not
// change favorites/groups no longer rebuild the screen.
final favoritos = context.select<EstadoRadio, List<Emisora>>(
(e) => e.listaFavoritos,
);
final grupos = context.select<EstadoRadio, List<GrupoFavoritos>>(
(e) => e.gruposFavoritos,
);
final l10n = AppLocalizations.of(context);
if (favoritos.isEmpty) {
@@ -49,16 +55,17 @@ class PantallaFavoritos extends StatelessWidget {
);
}
final gruposVisibles = grupos.isEmpty
? [
GrupoFavoritos(
id: GrupoFavoritos.sinAsignarId,
nombre: l10n.favoriteGroupsUnassigned,
orden: 0,
protegido: true,
),
]
: grupos;
final gruposVisibles =
grupos.isEmpty
? [
GrupoFavoritos(
id: GrupoFavoritos.sinAsignarId,
nombre: l10n.favoriteGroupsUnassigned,
orden: 0,
protegido: true,
),
]
: grupos;
return CustomScrollView(
slivers: [
@@ -86,9 +93,10 @@ class PantallaFavoritos extends StatelessWidget {
_GrupoFavoritosPanel(
grupo: grupo,
grupos: gruposVisibles,
emisoras: favoritos
.where((e) => e.grupoFavoritosId == grupo.id)
.toList(),
emisoras:
favoritos
.where((e) => e.grupoFavoritosId == grupo.id)
.toList(),
),
const SizedBox(height: 12),
],
@@ -125,7 +133,9 @@ class _GrupoFavoritosPanel extends StatelessWidget {
children: [
Row(
children: [
Icon(grupo.esSinAsignar ? Icons.lock_rounded : Icons.folder_rounded),
Icon(
grupo.esSinAsignar ? Icons.lock_rounded : Icons.folder_rounded,
),
const SizedBox(width: 8),
Expanded(
child: Text(
@@ -181,30 +191,31 @@ class _FavoritoItem extends StatelessWidget {
final seleccionado = await showModalBottomSheet<String>(
context: context,
showDragHandle: true,
builder: (ctx) => SafeArea(
child: ListView(
shrinkWrap: true,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 12),
child: Text(
l10n.favoriteGroupsAssign,
style: Theme.of(ctx).textTheme.titleLarge,
),
),
for (final grupo in grupos)
ListTile(
leading: Icon(
grupo.id == emisora.grupoFavoritosId
? Icons.radio_button_checked_rounded
: Icons.radio_button_off_rounded,
builder:
(ctx) => SafeArea(
child: ListView(
shrinkWrap: true,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 12),
child: Text(
l10n.favoriteGroupsAssign,
style: Theme.of(ctx).textTheme.titleLarge,
),
),
title: Text(_nombreVisible(l10n, grupo)),
onTap: () => Navigator.pop(ctx, grupo.id),
),
],
),
),
for (final grupo in grupos)
ListTile(
leading: Icon(
grupo.id == emisora.grupoFavoritosId
? Icons.radio_button_checked_rounded
: Icons.radio_button_off_rounded,
),
title: Text(_nombreVisible(l10n, grupo)),
onTap: () => Navigator.pop(ctx, grupo.id),
),
],
),
),
);
if (seleccionado == null || !context.mounted) return;
await context.read<EstadoRadio>().asignarGrupoFavorito(