AudioPeel follows MVVM + Repository with a clean three-layer separation.
┌─────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ screens/ widgets/common/ │
│ (8 screens) (8 reusable widgets) │
│ │
│ Reads state via context.watch<Provider>() │
│ Dispatches actions via context.read<Provider>().method() │
├─────────────────────────────────────────────────────────┤
│ BUSINESS LOGIC LAYER │
│ providers/ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ ConversionProvider│ │ HistoryProvider │ │
│ │ SettingsProvider │ │ AdProvider │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ Extends ChangeNotifier │
│ Orchestrates services, manages UI state │
├─────────────────────────────────────────────────────────┤
│ DATA / SERVICE LAYER │
│ services/ models/ │
│ ┌─────────────────┐ ┌───────────────────┐ │
│ │ FFmpegService │ │ AudioFile │ │
│ │ DatabaseService │ │ AudioQuality │ │
│ │ StorageService │ │ ConversionTask │ │
│ │ PermissionService│ └───────────────────┘ │
│ │ IapService │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
User taps "SELECT VIDEO"
→ ConversionProvider.selectVideo()
→ FilePicker → selects file
→ FFmpegService.getVideoMetadata() → durationMs, sizeBytes
→ notifyListeners() → UI updates
User taps "CONVERT NOW"
→ ConversionProvider.startConversion()
→ StorageService.hasEnoughSpace() → check disk
→ StorageService.uniqueOutputPath() → generate path
→ FFmpegService.convertVideoToMp3()
→ onProgress callback → provider.notifyListeners() → ring updates
→ timeout Timer (10 min max)
→ On success:
→ DatabaseService.insertConversion() → persist history
→ StorageService.deleteFile() (if autoDelete enabled)
→ On failure:
→ StorageService.deleteFile() → cleanup partial output
→ notifyListeners() → screen navigates to Success/Error
| Provider | Owns | Depends On |
|---|---|---|
ConversionProvider |
Video selection, conversion lifecycle, progress | FFmpegService, StorageService, DatabaseService |
HistoryProvider |
Conversion list, filters, search | DatabaseService |
SettingsProvider |
Dark mode, quality, normalise, auto-delete, ads-removed | SharedPreferences |
AdProvider |
Banner creation, interstitial load/show, frequency gating | google_mobile_ads |
IapService |
Purchase flow, restore, product details | in_app_purchase |
SplashScreen (1.5s, auto)
└→ HomeScreen
├→ ConversionOptionsScreen
│ └→ ConvertingScreen
│ ├→ ConversionSuccessScreen
│ └→ ConversionErrorScreen
├→ HistoryScreen
└→ SettingsScreen
No bottom navigation bar. All navigation is push/pop via Navigator.
TimerAll magic numbers live in lib/constants/:
app_colors.dart — colour paletteapp_strings.dart — all UI textapp_constants.dart — sizes, durations, ad IDs, pref keysapp_theme.dart — Material 3 ThemeData (light + dark)