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:
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_busqueda.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
|
||||
/// S4-R3: EstadoBusqueda owns search query, results and loading state
|
||||
/// previously in EstadoRadio.
|
||||
void main() {
|
||||
test('actualizar la búsqueda notifica a los listeners', () async {
|
||||
final busqueda = EstadoBusqueda(
|
||||
radio: FakeServicioRadio(
|
||||
busqueda: [emisoraDemo(uuid: 'b-1', nombre: 'Resultado Uno')],
|
||||
),
|
||||
);
|
||||
addTearDown(busqueda.dispose);
|
||||
|
||||
var notificaciones = 0;
|
||||
busqueda.addListener(() => notificaciones++);
|
||||
|
||||
await busqueda.buscar(nombre: 'uno');
|
||||
|
||||
// At least once for the loading flag and once for the results.
|
||||
expect(notificaciones, greaterThanOrEqualTo(2));
|
||||
expect(busqueda.cargando, isFalse);
|
||||
expect(busqueda.resultados.map((e) => e.uuid), contains('b-1'));
|
||||
});
|
||||
|
||||
test('cargarMas pagina resultados y acota memoria', () async {
|
||||
final emisoras = List.generate(
|
||||
70,
|
||||
(i) => emisoraDemo(uuid: 'page-$i', nombre: 'Page $i'),
|
||||
);
|
||||
final busqueda = EstadoBusqueda(
|
||||
radio: FakeServicioRadio(busqueda: emisoras),
|
||||
);
|
||||
addTearDown(busqueda.dispose);
|
||||
|
||||
await busqueda.buscar(nombre: 'page');
|
||||
expect(busqueda.resultados, hasLength(30));
|
||||
expect(busqueda.hayMas, isTrue);
|
||||
|
||||
await busqueda.cargarMas();
|
||||
expect(busqueda.resultados, hasLength(60));
|
||||
|
||||
await busqueda.cargarMas();
|
||||
expect(busqueda.resultados, hasLength(70));
|
||||
expect(busqueda.hayMas, isFalse);
|
||||
});
|
||||
|
||||
test(
|
||||
'resultados conserva identidad entre lecturas sin cambios (S4-R5)',
|
||||
() async {
|
||||
final busqueda = EstadoBusqueda(
|
||||
radio: FakeServicioRadio(
|
||||
busqueda: [emisoraDemo(uuid: 'b-1', nombre: 'Resultado Uno')],
|
||||
),
|
||||
);
|
||||
addTearDown(busqueda.dispose);
|
||||
|
||||
await busqueda.buscar(nombre: 'uno');
|
||||
|
||||
// Identity-stable getters let `context.select` skip rebuilds when the
|
||||
// underlying data did not change.
|
||||
expect(identical(busqueda.resultados, busqueda.resultados), isTrue);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_grabacion.dart';
|
||||
import 'package:pluriwave/modelos/emisora.dart';
|
||||
import 'package:pluriwave/servicios/servicio_grabacion_radio.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
|
||||
/// S4-R2: EstadoGrabacion owns the recording state previously in EstadoRadio
|
||||
/// and manages ServicioGrabacionRadio.
|
||||
void main() {
|
||||
test('notifica listeners cuando cambia el estado de grabación', () async {
|
||||
final servicio = _ServicioGrabacionControlado();
|
||||
final estado = EstadoGrabacion(servicio: servicio);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
var notificaciones = 0;
|
||||
estado.addListener(() => notificaciones++);
|
||||
|
||||
servicio.emitir(
|
||||
EstadoGrabacionRadio(
|
||||
tipo: EstadoGrabacionRadioTipo.grabando,
|
||||
emisora: emisoraDemo(uuid: 'rec-1', nombre: 'Grabable'),
|
||||
inicio: DateTime(2026, 6, 11, 10),
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(notificaciones, 1);
|
||||
expect(estado.activa, isTrue);
|
||||
expect(estado.estado.tipo, EstadoGrabacionRadioTipo.grabando);
|
||||
});
|
||||
|
||||
test('iniciar delega en el servicio con la emisora actual', () async {
|
||||
final servicio = _ServicioGrabacionControlado();
|
||||
final emisora = emisoraDemo(uuid: 'rec-2', nombre: 'Actual');
|
||||
final estado = EstadoGrabacion(
|
||||
servicio: servicio,
|
||||
emisoraActual: () => emisora,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await estado.iniciar(duracion: const Duration(minutes: 1));
|
||||
|
||||
expect(servicio.inicios, 1);
|
||||
expect(servicio.emisoraIniciada?.uuid, 'rec-2');
|
||||
expect(servicio.duracionIniciada, const Duration(minutes: 1));
|
||||
});
|
||||
|
||||
test(
|
||||
'iniciar sin emisora actual reporta error y no llama al servicio',
|
||||
() async {
|
||||
final servicio = _ServicioGrabacionControlado();
|
||||
final errores = <String>[];
|
||||
final estado = EstadoGrabacion(
|
||||
servicio: servicio,
|
||||
emisoraActual: () => null,
|
||||
alError: errores.add,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
await estado.iniciar();
|
||||
|
||||
expect(servicio.inicios, 0);
|
||||
expect(errores, hasLength(1));
|
||||
},
|
||||
);
|
||||
|
||||
test('un estado de error del servicio se reporta vía alError', () async {
|
||||
final servicio = _ServicioGrabacionControlado();
|
||||
final errores = <String>[];
|
||||
final estado = EstadoGrabacion(servicio: servicio, alError: errores.add);
|
||||
addTearDown(estado.dispose);
|
||||
|
||||
servicio.emitir(
|
||||
const EstadoGrabacionRadio(
|
||||
tipo: EstadoGrabacionRadioTipo.error,
|
||||
error: 'HTTP 500',
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(errores, hasLength(1));
|
||||
expect(errores.single, contains('HTTP 500'));
|
||||
});
|
||||
}
|
||||
|
||||
class _ServicioGrabacionControlado extends ServicioGrabacionRadio {
|
||||
final _controller = StreamController<EstadoGrabacionRadio>.broadcast();
|
||||
EstadoGrabacionRadio _estadoActual = const EstadoGrabacionRadio.inactiva();
|
||||
|
||||
int inicios = 0;
|
||||
Emisora? emisoraIniciada;
|
||||
Duration? duracionIniciada;
|
||||
|
||||
@override
|
||||
EstadoGrabacionRadio get estado => _estadoActual;
|
||||
|
||||
@override
|
||||
Stream<EstadoGrabacionRadio> get estadoStream => _controller.stream;
|
||||
|
||||
@override
|
||||
Future<void> inicializar() async {}
|
||||
|
||||
@override
|
||||
Future<void> iniciar(
|
||||
Emisora emisora, {
|
||||
Duration? duracion,
|
||||
String? directorio,
|
||||
}) async {
|
||||
inicios++;
|
||||
emisoraIniciada = emisora;
|
||||
duracionIniciada = duracion;
|
||||
}
|
||||
|
||||
void emitir(EstadoGrabacionRadio estado) {
|
||||
_estadoActual = estado;
|
||||
_controller.add(estado);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() => _controller.close();
|
||||
}
|
||||
@@ -60,7 +60,7 @@ void main() {
|
||||
await estado.inicializar();
|
||||
await estado.reproducir(emisora);
|
||||
|
||||
expect(estado.presetEcualizador, principal);
|
||||
expect(estado.ecualizador.presetActual, principal);
|
||||
expect(audio.presetsAplicados.first, principal);
|
||||
expect(audio.presetsAplicados.last, principal);
|
||||
},
|
||||
@@ -85,11 +85,11 @@ void main() {
|
||||
|
||||
await estado.inicializar();
|
||||
|
||||
expect(estado.ecualizadorDisponible, isFalse);
|
||||
expect(estado.presetEcualizador, principal);
|
||||
expect(estado.presetPrincipalEcualizador, principal);
|
||||
expect(estado.ecualizador.disponible, isFalse);
|
||||
expect(estado.ecualizador.presetActual, principal);
|
||||
expect(estado.ecualizador.presetPrincipal, principal);
|
||||
expect(
|
||||
estado.presetEcualizadorPorEmisora('fav-1'),
|
||||
estado.ecualizador.presetPorEmisora('fav-1'),
|
||||
PresetEcualizador.rock,
|
||||
);
|
||||
},
|
||||
@@ -151,15 +151,15 @@ void main() {
|
||||
|
||||
await estado.inicializar();
|
||||
await estado.cargarFavoritos();
|
||||
await estado.guardarPresetEcualizadorPorEmisora(emisora.uuid, propio);
|
||||
await estado.ecualizador.guardarPresetPorEmisora(emisora.uuid, propio);
|
||||
|
||||
await estado.reproducir(emisora);
|
||||
expect(estado.presetEcualizador, propio);
|
||||
expect(estado.ecualizador.presetActual, propio);
|
||||
expect(audio.presetsAplicados.last, propio);
|
||||
|
||||
await estado.deshabilitarPresetEcualizadorPorEmisora(emisora.uuid);
|
||||
await estado.ecualizador.deshabilitarPresetPorEmisora(emisora.uuid);
|
||||
await estado.reproducir(emisora);
|
||||
expect(estado.presetEcualizador, principal);
|
||||
expect(estado.ecualizador.presetActual, principal);
|
||||
expect(audio.presetsAplicados.last, principal);
|
||||
},
|
||||
);
|
||||
@@ -186,8 +186,8 @@ void main() {
|
||||
await estado.cargarFavoritos();
|
||||
await estado.reproducir(emisora);
|
||||
|
||||
expect(estado.tienePresetEcualizadorPorEmisora(emisora.uuid), isFalse);
|
||||
expect(estado.presetEcualizador, principal);
|
||||
expect(estado.ecualizador.tienePresetPorEmisora(emisora.uuid), isFalse);
|
||||
expect(estado.ecualizador.presetActual, principal);
|
||||
expect(audio.presetsAplicados.last, principal);
|
||||
},
|
||||
);
|
||||
@@ -207,15 +207,15 @@ void main() {
|
||||
);
|
||||
|
||||
await estado.inicializar();
|
||||
expect(estado.ecualizadorActivo, isTrue);
|
||||
expect(estado.ecualizador.activo, isTrue);
|
||||
|
||||
await estado.cambiarEcualizadorActivo(false);
|
||||
expect(estado.ecualizadorActivo, isFalse);
|
||||
await estado.ecualizador.cambiarActivo(false);
|
||||
expect(estado.ecualizador.activo, isFalse);
|
||||
expect(servicioEcualizador.config.activo, isFalse);
|
||||
expect(audio.cambiosEcualizadorActivo.last, isFalse);
|
||||
|
||||
await estado.cambiarEcualizadorActivo(true);
|
||||
expect(estado.ecualizadorActivo, isTrue);
|
||||
await estado.ecualizador.cambiarActivo(true);
|
||||
expect(estado.ecualizador.activo, isTrue);
|
||||
expect(servicioEcualizador.config.activo, isTrue);
|
||||
expect(audio.cambiosEcualizadorActivo.last, isTrue);
|
||||
},
|
||||
@@ -285,11 +285,11 @@ void main() {
|
||||
);
|
||||
|
||||
await estado.inicializar();
|
||||
await estado.guardarPresetEcualizadorPorEmisora(
|
||||
await estado.ecualizador.guardarPresetPorEmisora(
|
||||
primera.uuid,
|
||||
PresetEcualizador.rock,
|
||||
);
|
||||
await estado.guardarPresetEcualizadorPorEmisora(
|
||||
await estado.ecualizador.guardarPresetPorEmisora(
|
||||
segunda.uuid,
|
||||
PresetEcualizador.jazz,
|
||||
);
|
||||
@@ -301,7 +301,7 @@ void main() {
|
||||
audio.completar(primera.uuid);
|
||||
await primeraFuture;
|
||||
|
||||
expect(estado.presetEcualizador, PresetEcualizador.jazz);
|
||||
expect(estado.ecualizador.presetActual, PresetEcualizador.jazz);
|
||||
expect(radio.ultimoUuidClick, segunda.uuid);
|
||||
},
|
||||
);
|
||||
@@ -319,32 +319,8 @@ void main() {
|
||||
expect(lista.map((e) => e.orden).toList(), equals([0, 1, 2]));
|
||||
});
|
||||
|
||||
test('cargarMasBusqueda pagina resultados y acota memoria', () async {
|
||||
final emisoras = List.generate(
|
||||
70,
|
||||
(i) => emisoraDemo(uuid: 'page-$i', nombre: 'Page $i'),
|
||||
);
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(busqueda: emisoras),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
await estado.inicializar();
|
||||
await estado.buscar(nombre: 'page');
|
||||
expect(estado.resultadosBusqueda, hasLength(30));
|
||||
expect(estado.hayMasBusqueda, isTrue);
|
||||
|
||||
await estado.cargarMasBusqueda();
|
||||
expect(estado.resultadosBusqueda, hasLength(60));
|
||||
|
||||
await estado.cargarMasBusqueda();
|
||||
expect(estado.resultadosBusqueda, hasLength(70));
|
||||
expect(estado.hayMasBusqueda, isFalse);
|
||||
});
|
||||
// The search pagination test moved to test/estado/estado_busqueda_test.dart
|
||||
// (S4-R3: search state extracted to EstadoBusqueda).
|
||||
|
||||
test('toggleFavorito refresca lista global y evita estado stale', () async {
|
||||
final favoritos = FakeServicioFavoritos();
|
||||
@@ -388,16 +364,10 @@ void main() {
|
||||
final grupo = estado.gruposFavoritos.last;
|
||||
await estado.asignarGrupoFavorito(emisora.uuid, grupo.id);
|
||||
|
||||
expect(
|
||||
estado.listaFavoritos.first.grupoFavoritosId,
|
||||
grupo.id,
|
||||
);
|
||||
expect(estado.listaFavoritos.first.grupoFavoritosId, grupo.id);
|
||||
|
||||
await estado.eliminarGrupoFavoritos(grupo.id);
|
||||
expect(
|
||||
estado.listaFavoritos.first.grupoFavoritosId,
|
||||
'sin_asignar',
|
||||
);
|
||||
expect(estado.listaFavoritos.first.grupoFavoritosId, 'sin_asignar');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_busqueda.dart';
|
||||
import 'package:pluriwave/estado/estado_ecualizador.dart';
|
||||
import 'package:pluriwave/estado/estado_grabacion.dart';
|
||||
import 'package:pluriwave/estado/estado_radio.dart';
|
||||
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_inicio.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
|
||||
/// S4-R5-A: changing the EQ preset must NOT rebuild PantallaInicio.
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
testWidgets('cambiar el preset de EQ no marca PantallaInicio para rebuild', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(1440, 3200);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
final radio = FakeServicioRadio(
|
||||
populares: [emisoraDemo(uuid: 'api-1', nombre: 'API Uno')],
|
||||
popularesPorLlamada: [
|
||||
[emisoraDemo(uuid: 'api-1', nombre: 'API Uno')],
|
||||
[emisoraDemo(uuid: 'api-2', nombre: 'API Dos')],
|
||||
],
|
||||
);
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: radio,
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom:
|
||||
() async => File(
|
||||
'${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json',
|
||||
),
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estado.dispose);
|
||||
await tester.runAsync(estado.inicializar);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<EstadoRadio>.value(value: estado),
|
||||
ListenableProvider<EstadoEcualizador>.value(
|
||||
value: estado.ecualizador,
|
||||
),
|
||||
ListenableProvider<EstadoBusqueda>.value(value: estado.busqueda),
|
||||
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
|
||||
],
|
||||
child: MaterialApp(
|
||||
locale: const Locale('es'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: const Scaffold(body: PantallaInicio()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 100));
|
||||
|
||||
// Provider defers dependent notification to the next build phase, so a
|
||||
// dirty-flag probe cannot observe it synchronously. Instead, log every
|
||||
// element rebuilt per frame and look for the screen in that log.
|
||||
final registro = <String>[];
|
||||
final debugPrintOriginal = debugPrint;
|
||||
debugPrintRebuildDirtyWidgets = true;
|
||||
debugPrint = (String? message, {int? wrapWidth}) {
|
||||
registro.add(message ?? '');
|
||||
};
|
||||
addTearDown(() {
|
||||
debugPrintRebuildDirtyWidgets = false;
|
||||
debugPrint = debugPrintOriginal;
|
||||
});
|
||||
|
||||
// EQ preset change: a different notifier — must NOT rebuild the screen.
|
||||
await estado.ecualizador.cambiarPresetPrincipal(PresetEcualizador.rock);
|
||||
await tester.pump();
|
||||
expect(registro.any((linea) => linea.contains('PantallaInicio')), isFalse);
|
||||
|
||||
// Probe control: a real data change DOES rebuild the screen.
|
||||
registro.clear();
|
||||
await tester.runAsync(estado.cargarPopulares);
|
||||
await tester.pump();
|
||||
expect(registro.any((linea) => linea.contains('PantallaInicio')), isTrue);
|
||||
debugPrintRebuildDirtyWidgets = false;
|
||||
debugPrint = debugPrintOriginal;
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 100));
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,9 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_busqueda.dart';
|
||||
import 'package:pluriwave/estado/estado_ecualizador.dart';
|
||||
import 'package:pluriwave/estado/estado_grabacion.dart';
|
||||
import 'package:pluriwave/estado/estado_radio.dart';
|
||||
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_favoritos.dart';
|
||||
@@ -41,10 +44,7 @@ void main() {
|
||||
await tester.runAsync(estado.inicializar);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoRadio>.value(
|
||||
value: estado,
|
||||
child: _testApp(const PantallaInicio()),
|
||||
),
|
||||
_conProviders(estado, _testApp(const PantallaInicio())),
|
||||
);
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
@@ -109,10 +109,7 @@ void main() {
|
||||
await tester.runAsync(estado.inicializar);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoRadio>.value(
|
||||
value: estado,
|
||||
child: _testApp(const PantallaInicio()),
|
||||
),
|
||||
_conProviders(estado, _testApp(const PantallaInicio())),
|
||||
);
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
@@ -153,10 +150,7 @@ void main() {
|
||||
await tester.runAsync(estado.inicializar);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoRadio>.value(
|
||||
value: estado,
|
||||
child: _testApp(const PantallaInicio()),
|
||||
),
|
||||
_conProviders(estado, _testApp(const PantallaInicio())),
|
||||
);
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
@@ -176,10 +170,7 @@ void main() {
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoRadio>.value(
|
||||
value: estado,
|
||||
child: _testApp(const PantallaFavoritos()),
|
||||
),
|
||||
_conProviders(estado, _testApp(const PantallaFavoritos())),
|
||||
);
|
||||
await _pumpStableFrame(tester);
|
||||
|
||||
@@ -188,6 +179,20 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Mirrors the app.dart wiring: EstadoRadio owns the domain notifiers and
|
||||
/// the providers only expose the instances (no dispose callbacks).
|
||||
Widget _conProviders(EstadoRadio estado, Widget child) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<EstadoRadio>.value(value: estado),
|
||||
ListenableProvider<EstadoEcualizador>.value(value: estado.ecualizador),
|
||||
ListenableProvider<EstadoGrabacion>.value(value: estado.grabacion),
|
||||
ListenableProvider<EstadoBusqueda>.value(value: estado.busqueda),
|
||||
],
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _testApp(Widget body) {
|
||||
return MaterialApp(
|
||||
locale: const Locale('es'),
|
||||
|
||||
Reference in New Issue
Block a user