Chore — Purge Legacy ChangeNotifier Providers
Date: 2026-04-21
Scope: flow_mobile/lib/shared/providers/**, analyzer config, one test file move
Mobile branch: edit/mobile-audit-ux-polish · Docs branch: edit/web-brand-parity-docs
Commit subject: chore(providers): purge 6 orphan ChangeNotifier providers
Why
The audit’s Round 2 already removed the last call sites of the provider package (ChangeNotifierProvider from package:provider) in favor of Riverpod StateNotifiers. The ChangeNotifier classes themselves under lib/shared/providers/ were left behind — technically dead code, excluded from flutter analyze via analysis_options.yaml, and bloating the repo with 2,761 lines of unreferenced logic.
Keeping dead code around has real costs:
- Cognitive drag. A new contributor greps for
NotificationNotifier, finds two classes with the same name (theChangeNotifierorphan and theStateNotifiercanonical), and has to stop and reason about which one is “real.” - False signals. Static analysis exclusions mask genuine issues.
lib/shared/providers/**was excluded, so any real regression in that path would have been silently skipped — but also any subtly-broken reference to a name from those files (e.g. a misspelled import) would have slipped through. - Drift bait. Orphaned files tend to rot. Schema changes in
chat_model.dartcould cascade into the orphanchat_provider.dartatbuild_runnertime and produce confusing failures unrelated to the actual work.
What changed
Deleted (6 files, 2,761 lines)
| File | Lines | Former purpose |
|---|---|---|
lib/shared/providers/app_state_provider.dart | 477 | App lifecycle, connectivity, theme, settings |
lib/shared/providers/chat_provider.dart | 857 | Chat list + message CRUD |
lib/shared/providers/event_provider.dart | 826 | Event discovery, details, filters, RSVP |
lib/shared/providers/notification_provider.dart | 112 | Notification list + mark-as-read |
lib/shared/providers/socket_provider.dart | 399 | Socket.IO connection + presence/typing |
lib/shared/providers/tag_provider.dart | 90 | Ecosystem tags (music/vibe/activity) |
Canonical Riverpod replacements already in production for all six:
| Legacy ChangeNotifier | Canonical Riverpod notifier |
|---|---|
AppStateProvider | lib/shared/notifiers/app_state_notifier.dart |
ChatProvider | lib/shared/notifiers/chat_notifier.dart |
EventProvider | lib/shared/notifiers/event_notifier.dart + repositories |
NotificationNotifier¹ | lib/shared/notifiers/notification_notifier.dart (Riverpod) |
SocketProvider | Direct Supabase Realtime in feature notifiers (no Socket.IO) |
TagProvider | lib/shared/notifiers/tag_notifier.dart |
¹ Yes, both files declared class NotificationNotifier — a silent name collision. Only the Riverpod StateNotifier version was imported anywhere in lib/. Deleting the orphan resolves the ambiguity.
Moved & renamed
test/unit/providers/chat_provider_test.dart → test/unit/models/message_model_test.dart
The file was misnamed: it never imported ChatProvider, never instantiated it, and actually tested the Message model’s copyWith, equality, and MessageStatus enum coverage. The header comment explicitly said so. After deleting the orphan, leaving the file under providers/ with a name referencing a class that no longer exists would have been gratuitously confusing. Comments updated to reflect the new location and the migration path (chat state lives in ChatNotifier now).
Analyzer config
Removed - lib/shared/providers/** from the exclude list in analysis_options.yaml. The directory no longer exists, so the exclusion was a dangling reference. Side effect: if anyone ever recreates lib/shared/providers/ in the future, it will be analyzed by default — the correct behaviour.
How the verification was done
The headline question: is it safe to delete these files?
Two distinct grep passes established orphan status:
- Import grep —
import.*shared/providers/across the entire workspace → 0 matches. - Class-name grep —
AppStateProvider|ChatProvider|EventProvider|SocketProvider|TagProvideracross all.dartfiles → matches only inside the provider files themselves (self-references) and one comment in the renamed test file.
The NotificationNotifier case needed an extra step because the name is shared with the canonical Riverpod class. Checked notifications_screen.dart:278 (the only external user) and confirmed its import is from shared/notifiers/notification_notifier.dart, not the orphan.
One trap was visible in app_state_provider.dart: it exported two enums (AppLifecycleState, ConnectivityStatus), not just the class. Top-level enums re-exported from a ChangeNotifier file are easy to miss. A grep for those enum names turned up:
- The orphan provider (self-reference, safe to delete)
shared/notifiers/app_state_notifier.dart— redeclares both enums independently, so the Riverpod notifier has no dependency on the orphanfeatures/messaging/screens/chat_screen.dart:64— usesAppLifecycleStatefrom Flutter’sWidgetsBindingObserver, not ours
So the enums travelled with the orphan without creating any phantom references.
Verification
flutter analyze
# Analyzing flow_mobile…
# 1 issue found (pre-existing use_build_context_synchronously lint
# in squad_detail_sheet.dart — unrelated to this change).
flutter test
# All tests passed. (45 tests)No behavioural changes. Nothing imports these files; no widget tree or route depends on them; no code generation fans out from them.
Cumulative audit progress
Round 1 (previous sessions) removed the provider package and migrated call sites. Round 2 (previous session) deleted the legacy EventCard + HeroEventCard + CompactEventCard widgets. Round 3 (this session) completes the cleanup by removing the class definitions themselves.
Round-3 tally so far (this branch):
| Work item | Lines removed | Commit |
|---|---|---|
| Event card widget unification (L2) | 819 | 5c732bd |
| Orphan ChangeNotifier provider purge | 2,761 | this commit |
| Total this branch | 3,580 |
Lessons
- Dead code with an analyzer exclusion is worse than dead code without one. The exclusion mask hides genuine regressions in addition to silencing noise. If a block of code is dead, delete it; if it’s alive, analyze it. Excluding it permanently is a third option that should stay rare and deliberate.
- Name collisions between legacy and canonical code are a smell. Two
class NotificationNotifierdeclarations with different supertypes coexisted for months because no file imported both. That’s a latent footgun — IDE “go to definition” would land on whichever one resolved first, and refactoring tools would ignore the orphan. Unique names help humans and tools equally. - Enums travel with the class file.
AppStateProviderexportedAppLifecycleStateandConnectivityStatusalongside the class. Before deleting any “provider” file, grep for all top-level declarations — not just the class name — to avoid breaking callers that import an enum without realizing its source. - Test-file location carries meaning.
test/unit/providers/chat_provider_test.darttold the reader “this tests a provider.” It actually testedMessage. Moving it totest/unit/models/message_model_test.dartmade the directory structure honest again and gave future contributors a better discovery path. - Audits compound. Each sub-round (package removal → call site migration → class deletion) shrinks the surface area for the next round. Doing them in order — rather than trying to delete classes before removing all call sites — kept every intermediate state shippable.
Follow-ups (still deferred)
These remain open from the broader audit but are out of scope for this commit:
- M3 — Surface leaderboard/tickets on home screen (needs product decision on placement and priority vs. existing sections).
- M4 —
feed_screen.dartgod-screen split (~2h refactor, low risk, can be scheduled any time). - Crew/squad nomenclature unification — UI consistently says “crew”; code is mixed between
crew_*andsquad_*. A rename pass would remove the friction, but touches many files and deserves its own commit. - Runtime stress testing + fake DB entries — requires emulator and Supabase test project; held until a block of time is available to set those up together.