Chore — Rimozione provider e codice ChangeNotifier orfano pre-Riverpod

Commit: chore(deps): drop provider and mockito — remove orphan ChangeNotifier code and refactor test to ProviderScope · Branch: edit/mobile-audit-ux-polish · Audit: 2026-04-20 UX polish (finding L1, esteso)

Cosa non andava

L’audit (L1) aveva marcato provider: ^6.1.2 come “unused dependency”. La verifica ha mostrato una situazione più profonda:

  1. Il package era usato, ma solo da un test: test/widget/features/onboarding/profile_setup_screen_test.dart wrappava ProfileSetupScreen in un MultiProvider + ChangeNotifierProvider<AuthProvider>.value.
  2. AuthProvider era orfano in produzione: la classe AuthProvider extends ChangeNotifier esisteva in lib/shared/providers/auth_provider.dart (426 righe), ma nessun codice in lib/ la istanziava più — la app live usa authNotifierProvider (Riverpod StateNotifier).
  3. RegistrationWizardProvider era orfano in produzione: stesso pattern. La classe esisteva con una submit(BuildContext context, AuthProvider authProvider) method, ma il call site in registration_wizard_screen.dart chiama notifier.submit(context) di una classe DIVERSA (RegistrationWizardNotifier extends StateNotifier, in lib/features/auth/notifiers/registration_wizard_notifier.dart).
  4. Il test passava comunque: perché ProfileSetupScreen (che usa Riverpod) ignorava il wrapping MultiProvider; i test esercitavano solo navigation + validation (non auth state), quindi il MockAuthProvider era cerimonia morta.
  5. mockito era usato solo per mockare la classe orfana: quindi anche mockito era dead weight.

Decisioni

ElementoStatusDecisione
AuthProvider (ChangeNotifier, 426 righe)Orfano in prod, usato solo da test legacyRimosso (lib/shared/providers/auth_provider.dart)
RegistrationWizardProvider (ChangeNotifier)Orfano in prod, signature disallineata dal call siteRimosso (lib/features/auth/providers/registration_wizard_provider.dart)
auth_provider_test.dartTestava classe ora eliminataRimosso
profile_setup_screen_test.dartTestava screen Riverpod-based con wrapping MultiProvider fasulloRefattorizzato — ora usa ProviderScope diretto
provider: ^6.1.2 (pubspec)Nessun consumer rimastoRimosso
mockito: ^5.4.4 (pubspec)Nessun consumer rimastoRimosso

Classi orfane NON rimosse in questo pass

Durante la verifica sono emersi 6 altri ChangeNotifier in lib/shared/providers/ (AppStateProvider, ChatProvider, EventProvider, NotificationNotifier [shadow-name della versione Riverpod], SocketProvider, TagProvider). Non sono usati in prod (nessun call site esterno ai loro file), ma possono avere relazioni con altri test o codice ancora da verificare.

Out of scope per questo chore. Da gestire in un pass dedicato “purge legacy ChangeNotifier providers” dopo aver verificato che non siano referenziati in test/integrations.

Diff riassuntivo

test/widget/features/onboarding/profile_setup_screen_test.dart

Prima (wrapping artificiale):

import 'package:provider/provider.dart';
import 'package:mockito/mockito.dart';
import 'package:flow_app/shared/providers/auth_provider.dart';
 
class MockAuthProvider extends Mock implements AuthProvider { /* … */ }
 
Widget createTestWidget() => MultiProvider(
  providers: [ChangeNotifierProvider<AuthProvider>.value(value: mockAuthProvider)],
  child: MaterialApp(/* … */, home: ProfileSetupScreen()),
);

Dopo (wrapping canonico Riverpod):

import 'package:flutter_riverpod/flutter_riverpod.dart';
 
Widget createTestWidget() => const ProviderScope(
  child: MaterialApp(/* … */, home: ProfileSetupScreen()),
);

Non serve override di authNotifierProvider perché i test esercitano solo navigation + validation — non toccano auth state. Se in futuro un test esercita la path updateProfile, aggiungi ProviderScope(overrides: [authNotifierProvider.overrideWith(() => FakeAuthNotifier())]).

pubspec.yaml

-  # State Management
-  provider: ^6.1.2
+  # State Management (Riverpod is the single source of truth — see CLAUDE.md)
   riverpod: ^2.5.1
   # Testing
-  mockito: ^5.4.4
   integration_test:
     sdk: flutter

Rimosso

  • lib/shared/providers/auth_provider.dart
  • lib/features/auth/providers/registration_wizard_provider.dart
  • test/unit/providers/auth_provider_test.dart
  • lib/features/auth/providers/ (cartella vuota, auto-pruned)

Perché il test passava prima nonostante il wrapping sbagliato

Domanda giusta da farsi — “se il MultiProvider era fasullo, perché i test non fallivano?“.

Risposta: ProfileSetupScreen è ConsumerStatefulWidget + usa authNotifierProvider (Riverpod). Non consulta MAI Provider.of<AuthProvider>(context). Quindi il wrapping MultiProvider era pura decorazione — il widget non sapeva che esistesse.

I 4 test esercitavano:

  1. Step 1 render (Bio, Location, Website)
  2. Navigation a step 2 (Interests)
  3. Validation ”< 3 interests”
  4. Finish con 3 interessi selezionati (assert sull’assenza del msg di validation, NON sul successo della submit)

Nessuna di queste path toccava ref.read(authNotifierProvider). Quindi il mock era letteralmente inerte.

Il nuovo test Riverpod-based produce un log AuthService.updateProfile nello stack quando il Finish button viene tappato (perché Riverpod ora funziona DAVVERO, ma senza un utente reale la call fallisce gracefully). Il test continua a passare perché assert è sull’assenza della validation error, non sulla presenza del success.

Verifica

flutter analyze
# → 1 pre-existing info (squad_detail_sheet.dart context-across-async)
 
flutter test test/widget/features/onboarding/profile_setup_screen_test.dart
# → All tests passed!
 
flutter test test/unit/
# → All tests passed!
 
flutter pub get
# → "These packages are no longer being depended on:
#    - mockito 5.4.4
#    Changed 2 dependencies!"

Lessons

  • “Unused dependency” richiede full graph trace. Un flutter pub deps avrebbe mostrato provider come usato (dal test). La vera domanda è: “il codice che lo usa è ancora vivo?“. Nel nostro caso, i test testavano classi fossili — la dep era usata ma il sottostante era morto.
  • Test che passano senza testare nulla. Il caso qui è un classico “cargo cult testing”: il wrapping MultiProvider era stato copiato da un template, ma il widget sotto era migrato a Riverpod. I test continuavano a passare perché non esercitavano mai la path mockata. Lesson: se una verify() su un mock non è asserita, il mock è decorazione.
  • Migration tail è lunga. Il grosso della migrazione providerriverpod era stato fatto mesi fa (la app live è tutta Riverpod). Ma sono rimasti indietro: 2 classi orfane, 1 test mal-refattorizzato, 2 deps. Migration “complete” ≠ codebase pulita. Serve un pass dedicato dopo ogni big migration per spazzare il longtail.
  • Shadowing names è un red flag. Esistevano DUE classi NotificationNotifier: una ChangeNotifier orfana in lib/shared/providers/, una StateNotifier live in lib/shared/notifiers/. Il compile risolve per import path (zero ambiguità a livello di tipo), ma cognitivamente è un mine-field. Convention flow d’ora in avanti: un solo file può esportare una classe con un dato nome nel progetto. Shadowing = debt.
  • Rifattorizzare un test prima di eliminare il suo soggetto. Ordine corretto: (1) fai funzionare il test senza la classe da eliminare, (2) verifica che passi, (3) elimina la classe. Ordine sbagliato: elimina la classe, rompe il test, rippiegi sotto time-pressure. Questo pass ha seguito il primo.