This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
MediaDojo is a desktop podcast and radio player built with Flutter. It serves as an example application to showcase the flutter_it ecosystem (get_it, watch_it, command_it, listen_it) for building well-structured Flutter applications without code generation.
The app targets Linux and macOS desktop platforms and demonstrates clean architecture with separation between UI, business logic (Managers), and data access (Services).
# Run the app (Linux/macOS)
flutter run
# Build for Linux
flutter build linux -v
# Build for macOS
flutter build macos -v# Analyze code (strict mode with fatal infos)
flutter analyze --fatal-infos
# Format code (REQUIRED before commits per user preferences)
dart format .
# Check formatting without changes
dart format --set-exit-if-changed .
# Get dependencies
flutter pub getNote: Tests are currently commented out in CI. When adding tests:
# Run all tests
flutter test
# Run specific test file
flutter test test/path/to/test_file.dartThis project uses FVM (Flutter Version Manager):
# Current Flutter version: 3.35.5 (see .fvmrc)
fvm use 3.35.5
fvm flutter run
fvm flutter build linuxMediaDojo follows a three-layer architecture:
UI (Flutter Widgets) ← watch/call → Managers (Business Logic) → Services (Data Access) → Data Sources
1. UI Layer (lib/*/view/)
- Flutter widgets that display data and handle user interactions
- Uses
watch_itto reactively watch Manager state - Calls Commands on Managers (never directly calls Services)
- Must extend
WatchingWidgetorWatchingStatefulWidgetwhen using watch_it functions
2. Manager Layer (lib/*/*_manager.dart)
- Contains business logic encapsulated in
Commandobjects fromcommand_it - Registered as singletons in
get_it(lives for entire app lifetime) - Exposes Commands that UI can call and watch
- Orchestrates calls to Services
- No direct database/network access
3. Service Layer (lib/*/*_service.dart)
- Pure data access and business operations
- No UI dependencies
- Handles network requests, local storage, file I/O
- Can be pure Dart classes or use Flutter dependencies (e.g.,
ChangeNotifier)
All registered in lib/register_dependencies.dart:
- PodcastManager: Podcast search and episode fetching via Commands
- PlayerManager: Audio playback control (extends
BaseAudioHandler) - DownloadManager: Episode download orchestration (extends
ChangeNotifier) - RadioManager: Radio station management
- SettingsManager: User preferences
- SearchManager: Global search coordination
- CollectionManager: User's collection management
- PodcastService: Podcast search API integration (
podcast_searchpackage) - PodcastLibraryService: Podcast subscriptions storage (
SharedPreferences) - RadioService: Radio station data fetching (
radio_browser_api) - RadioLibraryService: Radio favorites storage
- SettingsService: Persistent user settings
- NotificationsService: Local notifications
- OnlineArtService: Album art fetching
See lib/register_dependencies.dart for the complete setup. Key patterns:
// Singleton with async initialization
di.registerSingletonAsync<ServiceName>(
() async => ServiceName(),
dependsOn: [OtherService],
);
// Lazy singleton (created on first access)
di.registerLazySingleton<ServiceName>(
() => ServiceName(),
dispose: (s) => s.dispose(),
);
// Singleton with dependencies
di.registerSingletonWithDependencies<Manager>(
() => Manager(service: di<ServiceName>()),
dependsOn: [ServiceName],
);IMPORTANT: Use di (alias for GetIt.instance) throughout the codebase, not GetIt.instance directly.
Commands wrap functions and provide reactive state. UI can call them and watch for results/errors separately:
// In Manager
late Command<String?, SearchResult> updateSearchCommand;
updateSearchCommand = Command.createAsync<String?, SearchResult>(
(query) async => _podcastService.search(searchQuery: query),
initialValue: SearchResult(items: []),
);
// In UI
di<PodcastManager>().updateSearchCommand.run('flutter');
// Watch results elsewhere
final results = watchValue((context) =>
di<PodcastManager>().updateSearchCommand.value
);CRITICAL: When using any watch_it functions (watch, watchValue, callOnce, createOnce, registerHandler):
- Widget MUST extend
WatchingWidgetorWatchingStatefulWidget - All watch calls MUST be in the same order on every build
- Never conditionally call watch functions
Managers are singletons that live for the entire app lifetime:
- No need to dispose Commands or subscriptions manually
- They're cleaned up when the app process terminates
- Document this clearly in Manager classes (see
PodcastManagerfor example)
Use listen_it operators for reactive transformations:
// Debounce search input
searchManager.textChangedCommand
.debounce(const Duration(milliseconds: 500))
.listen((filterText, sub) => updateSearchCommand.run(filterText));The project uses strict linting rules (see analysis_options.yaml):
- Single quotes for strings
- Const constructors where possible
- Trailing commas required
- Relative imports within the project
- No print statements - use logging utilities
- Cancel subscriptions properly
- Close sinks when done
Key rules to follow:
prefer_single_quotes: trueprefer_const_constructors: truerequire_trailing_commas: trueprefer_relative_imports: trueavoid_print: truecancel_subscriptions: true
- Window Management: Uses
window_managerpackage for window control - System Theme: Uses
system_themefor accent colors - Audio Backend: Uses
media_kitwith MPV for audio/video playback - Linux Dependencies: Requires libmpv-dev, libnotify-dev, libgtk-3-dev
- Audio Service: Integrates with desktop media controls via
audio_service
Use lib/common/platforms.dart for platform checks:
if (Platforms.isLinux) { ... }
if (Platforms.isMacOS) { ... }
if (Platforms.isDesktop) { ... }lib/
├── app/ # App initialization and main widget
│ ├── app.dart # Root widget
│ ├── home.dart # Main navigation scaffold
│ └── app_config.dart # App constants
├── common/ # Shared utilities and widgets
│ ├── view/ # Reusable UI components
│ ├── platforms.dart # Platform detection
│ └── logging.dart # Debug logging utilities
├── podcasts/ # Podcast feature
│ ├── podcast_manager.dart
│ ├── podcast_service.dart
│ ├── podcast_library_service.dart
│ ├── download_manager.dart
│ ├── data/ # Podcast data models
│ └── view/ # Podcast UI widgets
├── radio/ # Radio feature
│ ├── radio_manager.dart
│ ├── radio_service.dart
│ └── view/
├── player/ # Audio player feature
│ ├── player_manager.dart
│ ├── data/ # Player state models
│ └── view/ # Player UI
├── settings/ # Settings feature
│ ├── settings_manager.dart
│ ├── settings_service.dart
│ └── view/
├── search/ # Global search
│ └── search_manager.dart
├── collection/ # User collections
│ └── collection_manager.dart
├── extensions/ # Dart extension methods
├── l10n/ # Localization (generated)
└── register_dependencies.dart # Dependency injection setup
- flutter_it: Umbrella package containing get_it, watch_it, command_it, listen_it
- media_kit: Audio/video playback engine
- podcast_search: Podcast search API client
- radio_browser_api: Radio station database API
- audio_service: OS media controls integration
- shared_preferences: Local key-value storage
- dio: HTTP client for downloads
- window_manager: Desktop window control
- yaru: Ubuntu-style widgets and theming
Several dependencies are pinned to specific git commits for stability.
The app uses a hierarchy of media types for playback:
- UniqueMedia (base class): Represents any playable media
- EpisodeMedia: Podcast episodes
- StationMedia: Radio stations
- LocalMedia: Local audio files
All found in lib/player/data/.
- PodcastMetadata: Extended podcast info with subscription state
- Item: From
podcast_searchpackage (podcast or episode) - Episode: From
podcast_searchpackage
- DownloadCapsule: Encapsulates episode + download directory for download operations
- Managers:
{feature}_manager.dart(e.g.,podcast_manager.dart) - Services:
{feature}_service.dart(e.g.,podcast_service.dart) - Views:
lib/{feature}/view/{widget_name}.dart - Data Models:
lib/{feature}/data/{model_name}.dart - Extensions:
lib/extensions/{type}_x.dart(e.g.,string_x.dart)
- Before making changes: Read existing code to understand patterns
- Run analyzer:
flutter analyze --fatal-infosto catch issues - Format code:
dart format .before committing (REQUIRED per user rules) - Test locally: Run the app to verify changes
- Never commit without asking (per user's global rules)
GitHub Actions workflows (.github/workflows/):
-
ci.yaml: Runs on PRs
- Analyzes with
flutter analyze --fatal-infos - Checks formatting with
dart format --set-exit-if-changed - Builds Linux binary (requires Rust toolchain and system dependencies)
- Tests are currently disabled
- Analyzes with
-
release.yml: Handles release builds
- No code generation: This project intentionally avoids build_runner and code gen
- No tests currently: Test infrastructure exists but tests are commented out
- Desktop only: No mobile support (Android/iOS specific code paths exist but are unused)
- Global exception handling:
Command.globalExceptionHandleris set inPodcastManager - FVM required: Use FVM to ensure correct Flutter version (3.35.5)
- ❌ Don't call Services directly from UI - always go through Managers
- ❌ Don't use
GetIt.instancedirectly - usedialias - ❌ Don't forget to extend
WatchingWidget/WatchingStatefulWidgetwhen using watch_it - ❌ Don't conditionally call watch_it functions
- ❌ Don't add watch_it functions in different orders across rebuilds
- ❌ Don't use
print()- useprintMessageInDebugMode()fromcommon/logging.dart - ❌ Don't commit without formatting code first
- ❌ Don't create new features without following the Manager → Service pattern