From e58a12ee773da39d639ebb0e26fdb1b782941c56 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 21 May 2026 18:35:26 +0300 Subject: Wrap AudioPlayerScreen in audio_service for background playback (1b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creates PlayerAudioHandler (BaseAudioHandler + SeekHandler) that wraps just_audio's AudioPlayer and bridges it to the Android media session: background foreground-service playback, lock-screen / notification controls (play/pause/seek/skip ±15 s), audio focus, and Bluetooth headset events all handled by audio_service. Key design decisions: - Handler registered once via AudioService.init in main() and injected into ProviderScope via overrideWithValue (DIP: no global mutable var). - Progress-sync timer stays in the screen so PlayerApiClient is never imported by the handler (SRP boundary preserved). - Seek bar onChanged routes through handler.seek() so the notification position updates on slider drags (Law of Demeter fix). - _initPlayer refactored into _buildAuthHeaders / _loadSource / _resumeFromSavedPosition helpers (each ≤30 lines, SoC). - Tests override audioHandlerProvider with _FakePlayerAudioHandler to avoid platform-channel calls; all 221 tests pass. Co-Authored-By: Claude Sonnet 4.6 --- player-android/lib/main.dart | 56 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) (limited to 'player-android/lib/main.dart') diff --git a/player-android/lib/main.dart b/player-android/lib/main.dart index 156b8f8..b32c8e7 100644 --- a/player-android/lib/main.dart +++ b/player-android/lib/main.dart @@ -1,11 +1,61 @@ +import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:just_audio/just_audio.dart'; +import 'providers/audio_handler_provider.dart'; import 'router.dart'; +import 'services/audio_handler.dart'; -/// Entry point — wraps the whole widget tree in a [ProviderScope] so every -/// widget and provider has access to the Riverpod container. -void main() => runApp(const ProviderScope(child: PlayerAndroidApp())); +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- + +/// Entry point. +/// +/// [AudioService.init] must be called before [runApp] so that the handler is +/// registered before any widget tries to obtain it via [audioHandlerProvider]. +/// +/// The handler is injected into [ProviderScope] via an override rather than +/// stored in a global variable, keeping the dependency explicit and testable +/// (Dependency Inversion Principle: the provider file does not need to import +/// `main.dart`; the composition root wires the graph). +void main() async { + // Ensure platform channels are ready before calling AudioService.init. + WidgetsFlutterBinding.ensureInitialized(); + + // Register the PlayerAudioHandler as the singleton media session handler. + // AudioService.init returns the exact T from the builder. We pass it + // directly into ProviderScope so [audioHandlerProvider] is resolved without + // any global mutable state. + final handler = await AudioService.init( + builder: () => PlayerAudioHandler(AudioPlayer()), + config: const AudioServiceConfig( + // Notification channel name shown in Android Settings → App info. + androidNotificationChannelName: 'Player Audio', + // Keep the service alive while the notification is visible so the OS + // does not kill the process when the user swipes the app away. + androidStopForegroundOnPause: false, + // Allow the user to swipe away the notification to stop playback (UX + // expectation on Android — mirrors Spotify / Podcast Addict behaviour). + androidNotificationOngoing: false, + ), + ); + + runApp( + ProviderScope( + // Override the provider with the concrete handler instance so that every + // widget and provider that reads [audioHandlerProvider] gets the same + // singleton without going through a global variable. + overrides: [audioHandlerProvider.overrideWithValue(handler)], + child: const PlayerAndroidApp(), + ), + ); +} + +// --------------------------------------------------------------------------- +// Root widget +// --------------------------------------------------------------------------- /// Root application widget. /// -- cgit v1.2.3