fix(favoritos): use the cross-version onReorder API and correct drag index math
Build & Deploy PluriWave / Análisis de código (push) Successful in 24s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m26s

The CI Flutter SDK predates v3.41 and only exposes ReorderableListView's
onReorder; the newer onReorderItem broke the build with three analyzer
errors. onReorder exists in both SDKs, so use it and compensate for its
pre-removal newIndex internally.

Fixing the call site surfaced a real ordering bug: _onReorder located the
target neighbour in the untrimmed global list, while ServicioFavoritos
.reordenar inserts into the list after the station is removed. Dragging a
station downwards past its neighbours therefore landed it one slot too far.
Adds a mid-list downward-drag test, the only case that separates the two
coordinate spaces.
This commit is contained in:
2026-07-29 18:35:08 +02:00
parent 40c2763061
commit c01c518541
3 changed files with 61 additions and 12 deletions
+18 -6
View File
@@ -58,23 +58,35 @@ class _PantallaFavoritosState extends State<PantallaFavoritos> {
/// global position [EstadoRadio.reordenarFavorito] expects, so a drag /// global position [EstadoRadio.reordenarFavorito] expects, so a drag
/// while a group chip is active still produces a coherent global order /// while a group chip is active still produces a coherent global order
/// (other groups' relative order is left untouched). /// (other groups' relative order is left untouched).
///
/// [newIndex] arrives in `ReorderableListView.onReorder`'s pre-removal
/// coordinate space: dragging downwards reports the slot the row would
/// occupy while it is still in the list. The logic below indexes into the
/// list AFTER the row is removed, so shift by one in that direction first.
void _onReorder( void _onReorder(
List<Emisora> filtrados, List<Emisora> filtrados,
List<Emisora> favoritos, List<Emisora> favoritos,
int oldIndex, int oldIndex,
int newIndex, int newIndex,
) { ) {
if (newIndex > oldIndex) newIndex -= 1;
final movido = filtrados[oldIndex]; final movido = filtrados[oldIndex];
final restantes = List<Emisora>.from(filtrados)..removeAt(oldIndex); final restantes = List<Emisora>.from(filtrados)..removeAt(oldIndex);
// `ServicioFavoritos.reordenar` removes the station first and THEN
// inserts at the index it is given, so the target index must be
// expressed in the global list WITHOUT the moved station. Locating the
// neighbour in the untrimmed list instead drifts by one whenever the
// moved station sits before it.
final globalSinMovido =
favoritos.where((e) => e.uuid != movido.uuid).toList();
final int nuevoIndiceGlobal; final int nuevoIndiceGlobal;
if (restantes.isEmpty) { if (restantes.isEmpty) {
nuevoIndiceGlobal = favoritos.length - 1; nuevoIndiceGlobal = globalSinMovido.length;
} else if (newIndex >= restantes.length) { } else if (newIndex >= restantes.length) {
nuevoIndiceGlobal = favoritos.indexWhere( nuevoIndiceGlobal =
(e) => e.uuid == restantes.last.uuid, globalSinMovido.indexWhere((e) => e.uuid == restantes.last.uuid) + 1;
);
} else { } else {
nuevoIndiceGlobal = favoritos.indexWhere( nuevoIndiceGlobal = globalSinMovido.indexWhere(
(e) => e.uuid == restantes[newIndex].uuid, (e) => e.uuid == restantes[newIndex].uuid,
); );
} }
@@ -217,7 +229,7 @@ class _PantallaFavoritosState extends State<PantallaFavoritos> {
onTap: _abrirFormularioEmisoraPersonalizada, onTap: _abrirFormularioEmisoraPersonalizada,
), ),
), ),
onReorderItem: onReorder:
(oldIndex, newIndex) => (oldIndex, newIndex) =>
_onReorder(filtrados, favoritos, oldIndex, newIndex), _onReorder(filtrados, favoritos, oldIndex, newIndex),
children: [ children: [
+2 -2
View File
@@ -44,8 +44,8 @@ void main() {
for (final locale in _auditedLocales) { for (final locale in _auditedLocales) {
final arb = readArb(locale); final arb = readArb(locale);
for (final key in realKeys(arb)) { for (final key in realKeys(arb)) {
if (!es.containsKey(key)) // Missing keys are arb_parity_test's job, not this one's.
continue; // arb_parity_test's job, not this one's if (!es.containsKey(key)) continue;
if (arb[key] == es[key] && if (arb[key] == es[key] &&
!identicalValueAllowlist.contains((locale, key))) { !identicalValueAllowlist.contains((locale, key))) {
unlisted.add('$locale/$key = "${arb[key]}"'); unlisted.add('$locale/$key = "${arb[key]}"');
+41 -4
View File
@@ -226,10 +226,13 @@ void main() {
find.byType(ReorderableListView), find.byType(ReorderableListView),
); );
// Drag the 3rd row (index 2, "Station C") to the 1st position (index // Drag the 3rd row (index 2, "Station C") to the 1st position (index
// 0) exercised via the real onReorderItem callback the widget wires // 0), exercised via the real onReorder callback the widget wires up.
// up. onReorderItem (not the deprecated onReorder) already adjusts // `onReorder` is the API present across Flutter versions (the newer
// newIndex for the removed item, so no manual index math here. // `onReorderItem` does not exist on the CI SDK), so it reports
lista.onReorderItem!(2, 0); // newIndex in the PRE-removal coordinate space; `_onReorder`
// compensates internally. Moving upwards needs no shift, which is why
// (2, 0) maps straight through.
lista.onReorder!(2, 0);
await pumpStable(tester); await pumpStable(tester);
expect(estado.listaFavoritosManual.map((e) => e.uuid).toList(), [ expect(estado.listaFavoritosManual.map((e) => e.uuid).toList(), [
@@ -247,6 +250,40 @@ void main() {
]); ]);
}); });
testWidgets('dragging the 1st item downwards lands it in the right slot', (
tester,
) async {
// Guards the pre-removal index compensation in `_onReorder`. Downward
// drags are the ONLY direction `ReorderableListView.onReorder` reports
// in the pre-removal coordinate space, so an off-by-one here would slip
// past the upward-drag test above entirely.
setLargeSurface(tester);
_suppressListTileInkAssertion();
final estado = await crearEstadoConFavoritos();
addTearDown(estado.dispose);
await tester.pumpWidget(buildScreen(estado));
await pumpStable(tester);
final lista = tester.widget<ReorderableListView>(
find.byType(ReorderableListView),
);
// Move "Station A" (index 0) into the MIDDLE slot. onReorder reports
// newIndex == 2 in the pre-removal space; `_onReorder` shifts it to 1.
// This specific case is what makes the test meaningful: dropping at the
// very end (0, 3) yields the same answer with or without the shift,
// because both land in the `newIndex >= restantes.length` branch. Only a
// mid-list drop separates the two.
lista.onReorder!(0, 2);
await pumpStable(tester);
expect(estado.listaFavoritosManual.map((e) => e.uuid).toList(), [
'b',
'a',
'c',
]);
});
testWidgets( testWidgets(
'swap_vert sort action applies OrdenEmisoras.nombre and re-renders ' 'swap_vert sort action applies OrdenEmisoras.nombre and re-renders '
'alphabetically', 'alphabetically',