Skip to content

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)”
  1. fvm flutter analyze — zero new warnings/errors vs Phase 0 baseline
  2. fvm flutter test — full suite green
  3. dart run build_runner build --delete-conflicting-outputs — after any move of a file containing part '*.g.dart', @HiveType, or route definitions
  4. iOS Simulator smoke-run (fvm flutter run on simulator — debug on simulator per CLAUDE.md): cold start, login, home load, content play, shorts, download check, deep-link cold start (Phases 4–6)
  5. Phase 8 additionally: device with pre-existing Hive downloads data

  • lib/core/app_navigation.dart — 100% commented-out go_router code referencing defunct deeptoplay_new
  • lib/presentation/pages/noInternet_screen.dart — only referenced by the file above; not in router
  • lib/presentation/pages/content_pages/ios_video_player.dart — 215 lines, zero references
  • lib/data/repositories/app_settings_repository_impl.dart — 0 bytes; orphaned sibling lib/data/models/app_settings_model.dart (no references)
  • lib/presentation/bloc/connectivity/connectivity_event.dart and connectivity_state.dart — fully commented out
  • Swapped auth file names: widgets/auth/auth_bottomsheet.dart and widgets/auth/auth_bottomsheet_widget.dart contain each other’s classes (AuthBottomSheet / AuthBottomSheetWidget). (Verified: the consent_dialog pair is not swapped — leave it.)
  • lib/TotalPlayTime.dart — PascalCase file at repo-lib root; 2 importers
  • presentation/bloc/connectivity/connectivity_bloc.dart contains ConnectivityCubit
  • bloc/watch_history/watch_history_screen_bloc.dart and bloc/series_episode/series_bloc.dart — file/folder/class misalignment
  • Watchlist sheets: widgets/bottomsheet_widget/watchlist_bottomsheet.dart (class WatchListBottomSheet) misplaced; watchlist_bottomsheet_widget.dart (class WatchlistBottomMenu) misnamed
  • Typo: global episode_lenght in core/constant/const.dart (2 consumers)
  • Duplicated pasted-twice doc comment on entitlement_token in const.dart
  • lib/domain/services/deeplink_services.dart — concrete class importing presentation/router; domain/services/in_app_purchase_service.dart is a blocker importer
  • lib/domain/services/in_app_purchase_service.dart — concrete class with fluttertoast + cupertino/auto_route dialogs
  • lib/data/helpers/content_helper.dart — 950 lines mixing pure logic with BuildContext dialogs/bottom sheets; used by 4 blocs
  • Remaining concrete services in domain/services/: NotificationServices, RemoteConfigService, FacebookDeferredDeeplinkService, AnalyticsServices, LocalProxyServer, plus local_server.dart
  • main.dart constructs SettingsDataSource and NotificationLocalDataSource directly instead of via GetIt
  • core/constant/const.dart — ~30 global ValueNotifiers plus global AppRouter instance; 82 consumers
  • core/constant/api_url_const.dart — defaults to staging URLs, uses non-const globals
  • Sentry DSN hardcoded in main.dart
  • ~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/ duplicates core/utils/ (network_helper + 4 marktag_* files)
  • data/datasources/ mixing local/remote; settings_data_source.dart mixes 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.dart navigates inside the bloc (has TODOs); 4 bloc files reach into getIt internally; 43 widget/page files use field-initializer getIt<...>()
  • genre_screen.dart wired to ArtistScreenBloc

Phase 0 — Baseline (no changes) — ✅ done 2026-09-17

Section titled “Phase 0 — Baseline (no changes) — ✅ done 2026-09-17”
  1. Record baseline: fvm flutter analyze (save output), fvm flutter test (save summary), git status clean. 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”).

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”
  1. Delete lib/core/app_navigation.dart (100% commented-out go_router code referencing defunct deeptoplay_new).
  2. Delete lib/presentation/pages/noInternet_screen.dart (only referenced by the file above; not in router).
  3. Delete lib/presentation/pages/content_pages/ios_video_player.dart (215 lines, zero references).
  4. Delete lib/data/repositories/app_settings_repository_impl.dart (0 bytes) and its orphaned sibling lib/data/models/app_settings_model.dart (grep confirmed no references).
  5. Delete lib/presentation/bloc/connectivity/connectivity_event.dart and connectivity_state.dart (fully commented out).
  6. In lib/main.dart (~lines 186–196): remove the duplicated KMS prefs-restore try block (keep one).
  7. Check test/ for mirror files of everything deleted and remove them.
  8. 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”
  1. 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.
  2. Fix the swapped auth files: git mv widgets/auth/auth_bottomsheet.dart → auth_bottomsheet_widget_tmp.dart, widgets/auth/auth_bottomsheet_widget.dart → auth_bottomsheet.dart (defines AuthBottomSheet), then rename tmp → auth_bottomsheet_widget.dart (defines AuthBottomSheetWidget). Update all imports repo-wide (grep both filenames). Note: the consent_dialog pair is not swapped — leave it.
  3. Rename presentation/bloc/connectivity/connectivity_bloc.dart → connectivity_cubit.dart (class is ConnectivityCubit); update imports + test mirrors.
  4. Bloc/folder alignment. As done: each file already matched its class (WatchHistoryScreenBloc, SeriesBloc); only the folder was off. Every other bloc/ 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/.
  5. Watchlist sheets. As done: WatchListBottomSheet is a static show() helper like its five siblings, so it stays in widgets/bottomsheet_widget/. The misnamed widgets/watchlist_bottomsheet/watchlist_bottomsheet_widget.dart (class WatchlistBottomMenu) moved next to it as widgets/bottomsheet_widget/watchlist_bottom_menu.dart; watchlist_bottomsheet/ is gone.
  6. Rename global episode_lenght → episode_length in core/constant/const.dart + its 2 consumers (content_tab_widget.dart, single_content_screen.dart).
  7. Remove the pasted-twice doc comment on entitlement_token in const.dart.
  8. 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”
  1. lib/core/constant/api_url_const.dart: make all URL globals const; 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. useStaging is bool.fromEnvironment('USE_STAGING') in lib/core/config/app_config.dart; it switches the static/dynamic hosts and the Turnstile site key. site_uri stays a mutable global because bootstrap and localization rewrite it.
  2. Move Sentry DSN out of main.dart into a new lib/core/config/app_config.dart reading String.fromEnvironment('SENTRY_DSN', defaultValue: <current dsn>) (keeps working unchanged unless overridden). As done: all four SENTRY_* defines moved; SENTRY_ENVIRONMENT defaults to staging under USE_STAGING. Defines are documented in README.md.
  3. 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)”
  1. 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.
  2. 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 so data has no Flutter-UI imports (or relocate those calls to the bloc/page callers). Register impl in injection_container.dart.
  3. content_helper: split lib/data/helpers/content_helper.dart (950 lines): keep pure, context-free logic (download-limit queries, content checks) in data/helpers/; move every BuildContext-taking, dialog/bottom-sheet-showing function → new lib/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.
  4. Remaining concrete services in domain/services/ (NotificationServices, RemoteConfigService, FacebookDeferredDeeplinkService, AnalyticsServices, LocalProxyServer, plus local_server.dart): one service per commit — for each: grep importers; if only outer layers use it, move the file to data/services/ (or presentation/ if it navigates/shows UI) and register in GetIt; if domain use cases call it, extract an abstract interface in domain/services/ and put the impl in data/services/. Do not batch these.
  5. main.dart: register SettingsDataSource and NotificationLocalDataSource in injection_container.dart and resolve via getIt instead of constructing directly.
  6. 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)”
  1. Create lib/core/state/ with cohesive modules, moving (not copying) each declaration exactly once, keeping every ValueNotifier as-is (no semantics change):
    • session_state.dart: current_user, userGlobal, entitlement_token, currentUserDeviceCount, currentUserStreamCount
    • connectivity_state.dart: internet_available, network_interface_available, is_wifi_connected, vpn_connected, current_ip
    • locale_state.dart: current_language, country, current_city, current_continent, current_timezone
    • home_feed_state.dart: homePageListGlobal, home_feed_revision, navItemListGlobal, popup_notifications_global, bottom_bar_index, tab_index, category_tab_index
    • player_state.dart: episode_length, is_shorts_initialized, cast_overlay_bottom_inset, is_android_tv
    • download_state.dart: download_over_wifi_only, selected_download_quality
    • app_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_enabled
    • prefs_keys.dart: languagePrefsKey, kmsTokenHeadersPrefsKey
  2. Router instance: delete AppRouter appRouter = AppRouter(); from the state files; register getIt.registerLazySingleton<AppRouter>(() => AppRouter()) in injection_container.dart; update MyApp in main.dart and the ~12 global-appRouter consumers to getIt<AppRouter>(). This also removes the presentation import from core state.
  3. Turn lib/core/constant/const.dart into a temporary barrel: only export the new files. All 82 consumers keep compiling unchanged. Run analyze + tests.
  4. (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.
  5. 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)”
  1. Group loose pages under feature folders (move mirror tests in test/ identically):
    • pages/artist/ ← artist_archive_screen.dart, artist_profile.dart
    • pages/subscription/ ← subscription_screen.dart
    • pages/search/ ← search_screen.dart
    • pages/profile/ ← choose_profile_screen.dart
    • pages/splash/ ← splash_screen.dart, loading_screen.dart
    • pages/misc/ ← vpn_screen.dart, device_limit_exceed_screen.dart
    • pages/collection/ ← collection_screen.dart
    • pages/web_view/ ← in_app_view/web_view.dart (delete the now-empty in_app_view/)
  2. Move player controllers out of widgets/: create lib/presentation/player/controllers/ ← widgets/player/{cast_controller,chorki_player_controller,remote_playback_controller,remote_playback_coordinator,frame_safe_notifier}.dart and widgets/player/airplay/airplay_controller.dart → presentation/player/controllers/airplay_controller.dart. Update imports (these are referenced by injection_container.dart and player widgets).
  3. data/datasources/: move notification_local_data_source.dart → data/datasources/local/. (Leave settings_data_source.dart split as a documented TODO — it mixes Dio + SharedPreferences.)
  4. Merge core/helper/ into core/utils/ (move network_helper.dart into core/network/ if it’s network-specific; move the 4 marktag_* files to core/utils/).
  5. 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)”
  1. 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.
  2. The 43 widget/page files using field-initializer getIt<...>(): leave as acceptable pattern; optionally convert the worst offenders (blocs reaching into getIt internally — 4 files found) to constructor injection. One file per commit.
  3. genre_screen.dart wired to ArtistScreenBloc: verify with team whether genre needs its own bloc; minimal fix is a clarifying rename/comment; full fix (new GenreScreenBloc) 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)”
  1. Hive entities in domain (cache_content_entity.dart(+.g.dart), cache_subtitle_entity.dart(+.g.dart), firebase_notification.dart): move to data/models/ keeping the exact same @HiveType(typeId: …) values so persisted data remains readable; move the .g.dart part files along; update adapter registrations in main.dart and 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.
  2. 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, VideoEntity suggested), updating mappers in data/models/ and repositories. Guard with the existing shorts tests; add mapper round-trip tests before merging.
  3. 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.

  • lib/main.dart — dedupe KMS block; GetIt for datasources; router from DI; Sentry DSN from config
  • lib/core/constant/const.dart — typo fix, dedupe comment, then convert to barrel (Phase 5)
  • lib/core/constant/api_url_const.dart — const URLs, prod default + --dart-define staging
  • lib/injection_container.dart — register moved services, AppRouter, datasources; sort imports
  • lib/domain/services/deeplink_services.dart — moved out (Phase 4)
  • lib/domain/services/in_app_purchase_service.dart — becomes abstract interface
  • lib/data/helpers/content_helper.dart — split; UI half extracted
  • 4 blocs using content_helper — repoint to presentation helper
  • ~12 files using global appRouter — switch to getIt<AppRouter>()
  • lib/presentation/bloc/connectivity/, bloc/watch_history/, bloc/series_episode/ — file/folder renames
  • Import updates across lib/ and test/ for every move (grep-driven)
  • lib/core/config/app_config.dart — dart-define–based env/Sentry config
  • lib/core/state/{session,connectivity,locale,home_feed,player,download,app_info}_state.dart, prefs_keys.dart
  • lib/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/kvideo absolute paths): unrelated to this refactor, but don’t touch pubspec.yaml paths; 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.