Architecture & Folder Structure Cleanup Plan
Architecture & Folder Structure Cleanup Plan
Section titled “Architecture & Folder Structure Cleanup Plan”Git discipline for every phase: one branch + PR per phase; one commit per numbered step so any regression can be reverted independently.
Per-Phase Verification (mandatory after every step)
Section titled “Per-Phase Verification (mandatory after every step)”fvm flutter analyze— zero new warnings/errors vs Phase 0 baselinefvm flutter test— full suite greendart run build_runner build --delete-conflicting-outputs— after any move of a file containingpart '*.g.dart',@HiveType, or route definitions- iOS Simulator smoke-run (
fvm flutter runon simulator — debug on simulator per CLAUDE.md): cold start, login, home load, content play, shorts, download check, deep-link cold start (Phases 4–6) - Phase 8 additionally: device with pre-existing Hive downloads data
Scout Findings
Section titled “Scout Findings”Dead code
Section titled “Dead code”lib/core/app_navigation.dart— 100% commented-out go_router code referencing defunctdeeptoplay_newlib/presentation/pages/noInternet_screen.dart— only referenced by the file above; not in routerlib/presentation/pages/content_pages/ios_video_player.dart— 215 lines, zero referenceslib/data/repositories/app_settings_repository_impl.dart— 0 bytes; orphaned siblinglib/data/models/app_settings_model.dart(no references)lib/presentation/bloc/connectivity/connectivity_event.dartandconnectivity_state.dart— fully commented out
Naming inconsistencies
Section titled “Naming inconsistencies”- Swapped auth file names:
widgets/auth/auth_bottomsheet.dartandwidgets/auth/auth_bottomsheet_widget.dartcontain each other’s classes (AuthBottomSheet/AuthBottomSheetWidget). (Verified: theconsent_dialogpair is not swapped — leave it.) lib/TotalPlayTime.dart— PascalCase file at repo-lib root; 2 importerspresentation/bloc/connectivity/connectivity_bloc.dartcontainsConnectivityCubitbloc/watch_history/watch_history_screen_bloc.dartandbloc/series_episode/series_bloc.dart— file/folder/class misalignment- Watchlist sheets:
widgets/bottomsheet_widget/watchlist_bottomsheet.dart(classWatchListBottomSheet) misplaced;watchlist_bottomsheet_widget.dart(classWatchlistBottomMenu) misnamed - Typo: global
episode_lenghtincore/constant/const.dart(2 consumers) - Duplicated pasted-twice doc comment on
entitlement_tokeninconst.dart
Layer violations (domain/data leaking)
Section titled “Layer violations (domain/data leaking)”lib/domain/services/deeplink_services.dart— concrete class importing presentation/router;domain/services/in_app_purchase_service.dartis a blocker importerlib/domain/services/in_app_purchase_service.dart— concrete class with fluttertoast + cupertino/auto_route dialogslib/data/helpers/content_helper.dart— 950 lines mixing pure logic withBuildContextdialogs/bottom sheets; used by 4 blocs- Remaining concrete services in
domain/services/:NotificationServices,RemoteConfigService,FacebookDeferredDeeplinkService,AnalyticsServices,LocalProxyServer, pluslocal_server.dart main.dartconstructsSettingsDataSourceandNotificationLocalDataSourcedirectly instead of via GetIt
Global-state god file
Section titled “Global-state god file”core/constant/const.dart— ~30 globalValueNotifiers plus globalAppRouterinstance; 82 consumerscore/constant/api_url_const.dart— defaults to staging URLs, uses non-const globals- Sentry DSN hardcoded in
main.dart
Folder structure issues
Section titled “Folder structure issues”- ~25 loose files directly in
presentation/pages/(artist, subscription, search, profile, splash, vpn, web view, collection, etc.) - Player controllers buried in
widgets/player/(cast_controller 2151 lines, chorki_player_controller, remote_playback_controller/coordinator, frame_safe_notifier, airplay_controller) core/helper/duplicatescore/utils/(network_helper + 4 marktag_* files)data/datasources/mixing local/remote;settings_data_source.dartmixes Dio + SharedPreferences- Hive entities in
domain/(cache_content_entity.dart(+.g),cache_subtitle_entity.dart(+.g),firebase_notification.dart) - 5 overlapping shorts/video entities:
shorts.dart,shorts_entity.dart,shorts_parent.dart,shorts_video_entity.dart,video_entity.dart - Entity suffix disharmony:
settings_entity.dart,user.dart,meta.dart/meta_type_entity.dart, etc. main_screen_bloc.dartnavigates inside the bloc (has TODOs); 4 bloc files reach intogetItinternally; 43 widget/page files use field-initializergetIt<...>()genre_screen.dartwired toArtistScreenBloc
Phase 0 — Baseline (no changes) — ✅ done 2026-09-17
Section titled “Phase 0 — Baseline (no changes) — ✅ done 2026-09-17”- Record baseline:
fvm flutter analyze(save output),fvm flutter test(save summary),git statusclean. These are the comparison points for all later phases.- Baseline on
main@ 5a33f885: analyze 0 errors / 259 warnings / 1686 infos; tests 876 passed, 2 failing already (category_screen_test“Songs paints no category strip”,feed_tabs_shell_test“a single-feed shell paints no strip at all”).
- Baseline on
Phase 1 — Dead code deletion (zero behavior change) — ✅ done, branch refactor/cleanup-phase-1-dead-code
Section titled “Phase 1 — Dead code deletion (zero behavior change) — ✅ done, branch refactor/cleanup-phase-1-dead-code”- Delete
lib/core/app_navigation.dart(100% commented-out go_router code referencing defunctdeeptoplay_new). - Delete
lib/presentation/pages/noInternet_screen.dart(only referenced by the file above; not in router). - Delete
lib/presentation/pages/content_pages/ios_video_player.dart(215 lines, zero references). - Delete
lib/data/repositories/app_settings_repository_impl.dart(0 bytes) and its orphaned siblinglib/data/models/app_settings_model.dart(grep confirmed no references). - Delete
lib/presentation/bloc/connectivity/connectivity_event.dartandconnectivity_state.dart(fully commented out). - In
lib/main.dart(~lines 186–196): remove the duplicated KMS prefs-restoretryblock (keep one). - Check
test/for mirror files of everything deleted and remove them. - Verify: analyze + tests. Smoke-run on iOS Simulator.
Phase 2 — Mechanical renames/moves (no logic changes) — ✅ done, branch refactor/cleanup-phase-2-renames
Section titled “Phase 2 — Mechanical renames/moves (no logic changes) — ✅ done, branch refactor/cleanup-phase-2-renames”git mv lib/TotalPlayTime.dart lib/core/utils/total_play_time.dart; update its 2 importers:presentation/pages/content_pages/video_player_screen.dart,presentation/widgets/player/remote_playback_progress_reporter.dart. Move mirror test if present.- Fix the swapped auth files:
git mvwidgets/auth/auth_bottomsheet.dart→auth_bottomsheet_widget_tmp.dart,widgets/auth/auth_bottomsheet_widget.dart→auth_bottomsheet.dart(definesAuthBottomSheet), then rename tmp →auth_bottomsheet_widget.dart(definesAuthBottomSheetWidget). Update all imports repo-wide (grep both filenames). Note: theconsent_dialogpair is not swapped — leave it. - Rename
presentation/bloc/connectivity/connectivity_bloc.dart→connectivity_cubit.dart(class isConnectivityCubit); update imports + test mirrors. - Bloc/folder alignment. As done: each file already matched its class (
WatchHistoryScreenBloc,SeriesBloc); only the folder was off. Every otherbloc/folder is named after its file prefix, so the folders were renamed instead of the classes:bloc/watch_history/→bloc/watch_history_screen/,bloc/series_episode/→bloc/series/. - Watchlist sheets. As done:
WatchListBottomSheetis a staticshow()helper like its five siblings, so it stays inwidgets/bottomsheet_widget/. The misnamedwidgets/watchlist_bottomsheet/watchlist_bottomsheet_widget.dart(classWatchlistBottomMenu) moved next to it aswidgets/bottomsheet_widget/watchlist_bottom_menu.dart;watchlist_bottomsheet/is gone. - Rename global
episode_lenght→episode_lengthincore/constant/const.dart+ its 2 consumers (content_tab_widget.dart,single_content_screen.dart). - Remove the pasted-twice doc comment on
entitlement_tokeninconst.dart. - Verify: analyze + tests.
Phase 3 — Config hygiene — ✅ done, branch refactor/cleanup-phase-3-config
Section titled “Phase 3 — Config hygiene — ✅ done, branch refactor/cleanup-phase-3-config”lib/core/constant/api_url_const.dart: make all URL globalsconst; staging opt-in via--dart-define=USE_STAGING=true. As done: the default was already production (staging hosts were commented out), so no default changed.useStagingisbool.fromEnvironment('USE_STAGING')inlib/core/config/app_config.dart; it switches the static/dynamic hosts and the Turnstile site key.site_uristays a mutable global because bootstrap and localization rewrite it.- Move Sentry DSN out of
main.dartinto a newlib/core/config/app_config.dartreadingString.fromEnvironment('SENTRY_DSN', defaultValue: <current dsn>)(keeps working unchanged unless overridden). As done: all fourSENTRY_*defines moved;SENTRY_ENVIRONMENTdefaults tostagingunderUSE_STAGING. Defines are documented inREADME.md. - Verify: analyze + tests + manual smoke on simulator (confirm API host in logs).
Phase 4 — Layer violations (the two worst leaks first)
Section titled “Phase 4 — Layer violations (the two worst leaks first)”- Deeplink: move
lib/domain/services/deeplink_services.dart(concrete, imports presentation/router) →lib/presentation/navigation/deeplink_services.dart. First grep all importers:in_app_purchase_service.dart(domain) is the blocker — do step 22 in the same commit. - InAppPurchaseService: create abstract
lib/domain/services/in_app_purchase_service.dart(interface with the methods domain callers need), move the concrete class →lib/data/services/in_app_purchase_service_impl.dart, and move its UI side effects (fluttertoast, cupertino/auto_route dialogs) behind callbacks/params sodatahas no Flutter-UI imports (or relocate those calls to the bloc/page callers). Register impl ininjection_container.dart. - content_helper: split
lib/data/helpers/content_helper.dart(950 lines): keep pure, context-free logic (download-limit queries, content checks) indata/helpers/; move everyBuildContext-taking, dialog/bottom-sheet-showing function → newlib/presentation/helpers/content_ui_helper.dart. Update the 4 consuming blocs (content_screen_bloc,home_screen_bloc,series_bloc,shorts_screen_bloc) to call the presentation helper (blocs are presentation, so this is legal). Optional follow-up (separate commit): convert sheet-showing into emitted states handled by page listeners. - Remaining concrete services in
domain/services/(NotificationServices,RemoteConfigService,FacebookDeferredDeeplinkService,AnalyticsServices,LocalProxyServer, pluslocal_server.dart): one service per commit — for each: grep importers; if only outer layers use it, move the file todata/services/(orpresentation/if it navigates/shows UI) and register in GetIt; if domain use cases call it, extract an abstract interface indomain/services/and put the impl indata/services/. Do not batch these. main.dart: registerSettingsDataSourceandNotificationLocalDataSourceininjection_container.dartand resolve viagetItinstead of constructing directly.- Verify after each step: analyze + tests; after steps touching DI: full smoke-run (login, home load, deep link cold start).
Phase 5 — Split the global-state god-file (const.dart, 82 consumers)
Section titled “Phase 5 — Split the global-state god-file (const.dart, 82 consumers)”- Create
lib/core/state/with cohesive modules, moving (not copying) each declaration exactly once, keeping everyValueNotifieras-is (no semantics change):session_state.dart:current_user,userGlobal,entitlement_token,currentUserDeviceCount,currentUserStreamCountconnectivity_state.dart:internet_available,network_interface_available,is_wifi_connected,vpn_connected,current_iplocale_state.dart:current_language,country,current_city,current_continent,current_timezonehome_feed_state.dart:homePageListGlobal,home_feed_revision,navItemListGlobal,popup_notifications_global,bottom_bar_index,tab_index,category_tab_indexplayer_state.dart:episode_length,is_shorts_initialized,cast_overlay_bottom_inset,is_android_tvdownload_state.dart:download_over_wifi_only,selected_download_qualityapp_info_state.dart:app_version,android_package_id,ios_app_id,deeplink_url,pending_guarded_route,app_main_scaffold_ready,is_auth_bottomsheet_opened,isNotificationRouteInitiated,push_to_subscription,kms_token_headers_enabledprefs_keys.dart:languagePrefsKey,kmsTokenHeadersPrefsKey
- Router instance: delete
AppRouter appRouter = AppRouter();from the state files; registergetIt.registerLazySingleton<AppRouter>(() => AppRouter())ininjection_container.dart; updateMyAppinmain.dartand the ~12 global-appRouterconsumers togetIt<AppRouter>(). This also removes the presentation import from core state. - Turn
lib/core/constant/const.dartinto a temporary barrel: onlyexportthe new files. All 82 consumers keep compiling unchanged. Run analyze + tests. - (Gradual, separate commits, optional to complete in this effort): migrate consumers to direct imports of the new modules; delete the barrel when the last consumer is migrated.
- Verify: analyze + full test suite + smoke-run (this touches 82 files’ transitive imports — run the E2E suite in
core/e2e/if available).
Phase 6 — Folder restructuring (moves only, no logic edits)
Section titled “Phase 6 — Folder restructuring (moves only, no logic edits)”- Group loose pages under feature folders (move mirror tests in
test/identically):pages/artist/←artist_archive_screen.dart,artist_profile.dartpages/subscription/←subscription_screen.dartpages/search/←search_screen.dartpages/profile/←choose_profile_screen.dartpages/splash/←splash_screen.dart,loading_screen.dartpages/misc/←vpn_screen.dart,device_limit_exceed_screen.dartpages/collection/←collection_screen.dartpages/web_view/←in_app_view/web_view.dart(delete the now-emptyin_app_view/)
- Move player controllers out of
widgets/: createlib/presentation/player/controllers/←widgets/player/{cast_controller,chorki_player_controller,remote_playback_controller,remote_playback_coordinator,frame_safe_notifier}.dartandwidgets/player/airplay/airplay_controller.dart→presentation/player/controllers/airplay_controller.dart. Update imports (these are referenced byinjection_container.dartand player widgets). data/datasources/: movenotification_local_data_source.dart→data/datasources/local/. (Leavesettings_data_source.dartsplit as a documented TODO — it mixes Dio + SharedPreferences.)- Merge
core/helper/intocore/utils/(movenetwork_helper.dartintocore/network/if it’s network-specific; move the 4marktag_*files tocore/utils/). - Verify: analyze + tests + build_runner (in case any moved file has generated parts).
Phase 7 — DI/navigation hygiene (gradual, can overlap Phase 6)
Section titled “Phase 7 — DI/navigation hygiene (gradual, can overlap Phase 6)”main_screen_bloc.dart: it navigates inside the bloc (has TODOs). Move navigation into emitted states handled by a listener in the page; keep bloc pure.- The 43 widget/page files using field-initializer
getIt<...>(): leave as acceptable pattern; optionally convert the worst offenders (blocs reaching intogetItinternally — 4 files found) to constructor injection. One file per commit. genre_screen.dartwired toArtistScreenBloc: verify with team whether genre needs its own bloc; minimal fix is a clarifying rename/comment; full fix (newGenreScreenBloc) is a separate feature task, not part of cleanup.
Phase 8 — Highest-risk items (only after everything above is green)
Section titled “Phase 8 — Highest-risk items (only after everything above is green)”- Hive entities in domain (
cache_content_entity.dart(+.g.dart),cache_subtitle_entity.dart(+.g.dart),firebase_notification.dart): move todata/models/keeping the exact same@HiveType(typeId: …)values so persisted data remains readable; move the.g.dartpart files along; update adapter registrations inmain.dartand all imports; re-run build_runner. Test on a device/simulator that already has downloaded content to prove boxes still decode. Alternative if risk is unacceptable: leave in place with a documented ADR noting the tradeoff. - Shorts/video entity consolidation (5 overlapping:
shorts.dart,shorts_entity.dart,shorts_parent.dart,shorts_video_entity.dart,video_entity.dart): first produce a usage map (who constructs/serializes each), then merge to a minimal set (ShortsFeed,ShortsItem,VideoEntitysuggested), updating mappers indata/models/and repositories. Guard with the existing shorts tests; add mapper round-trip tests before merging. - Entity suffix harmonization (
settings_entity.dart→Settings,user.dart→User,meta.dart/meta_type_entity.dart, etc.): optional; only rename when a file is otherwise touched, to avoid mass churn.
Files to Modify
Section titled “Files to Modify”lib/main.dart— dedupe KMS block; GetIt for datasources; router from DI; Sentry DSN from configlib/core/constant/const.dart— typo fix, dedupe comment, then convert to barrel (Phase 5)lib/core/constant/api_url_const.dart—constURLs, prod default +--dart-definestaginglib/injection_container.dart— register moved services,AppRouter, datasources; sort importslib/domain/services/deeplink_services.dart— moved out (Phase 4)lib/domain/services/in_app_purchase_service.dart— becomes abstract interfacelib/data/helpers/content_helper.dart— split; UI half extracted- 4 blocs using content_helper — repoint to presentation helper
- ~12 files using global
appRouter— switch togetIt<AppRouter>() lib/presentation/bloc/connectivity/,bloc/watch_history/,bloc/series_episode/— file/folder renames- Import updates across
lib/andtest/for every move (grep-driven)
New Files
Section titled “New Files”lib/core/config/app_config.dart— dart-define–based env/Sentry configlib/core/state/{session,connectivity,locale,home_feed,player,download,app_info}_state.dart,prefs_keys.dartlib/presentation/navigation/deeplink_services.dart(moved)lib/presentation/helpers/content_ui_helper.dart(extracted from content_helper)lib/data/services/in_app_purchase_service_impl.dart(+ abstract interfaces for other migrated services)lib/presentation/player/controllers/(new home for 6 controllers)
Behavior flip in Phase 3: did not apply — production was already the default.- 82-file blast radius of const.dart split: mitigated by the temporary barrel export — zero consumer edits required to stay green; migration is gradual.
- Hive entity move (Phase 8): persisted-data corruption if typeIds change — plan pins typeIds and requires testing against a device with existing downloads; keep the “leave in domain” fallback.
- InAppPurchaseService UI extraction: easy to break purchase flows — do behind interface + keep UI behavior identical; test purchase path on sandbox account.
- Generated code: moving any
part '*.g.dart'file or route-bearing file without re-running build_runner produces confusing compile errors — build_runner is mandated after every such move. - Test tree drift: tests mirror
lib/; every move must include its test mirror or CI breaks. - Machine-specific player deps (
ottstack_player/kvideoabsolute paths): unrelated to this refactor, but don’t touchpubspec.yamlpaths; builds only work on this machine. - Large-file splits (cast_controller 2151 lines etc.): intentionally out of scope for this plan — restructuring first makes later splits safer; splitting state machines risks subtle cast regressions.