Common Development Tasks
This guide provides concrete recipes for common development workflows in ShonenX.
1. Adding a New Feature Slice
When introducing a new domain feature (e.g. a "Statistics" screen), follow the vertical slice pattern:
Step 1: Create the Feature Structure
Under lib/features/statistics/, create the standard directories:
lib/features/statistics/
├── domain/ # Data classes (e.g., stats_summary.dart)
├── data/ # Repositories & API fetchers
├── providers/ # Riverpod Notifiers (stats_provider.dart)
└── presentation/ # Flutter screens & widgets (stats_screen.dart)Step 2: Define the Domain Model
In domain/stats_summary.dart:
dart
class StatsSummary {
final int totalEpisodesWatched;
final Duration totalTimeSpent;
final Map<String, int> genreBreakdown;
const StatsSummary({
required this.totalEpisodesWatched,
required this.totalTimeSpent,
required this.genreBreakdown,
});
}Step 3: Wire the Riverpod Provider
In providers/stats_provider.dart:
dart
final statsProvider = FutureProvider.autoDispose<StatsSummary>((ref) async {
final historyRepo = ref.watch(historyRepositoryProvider);
return historyRepo.calculateUserStats();
});Step 4: Build the Presentation Widget
In presentation/stats_screen.dart:
dart
class StatsScreen extends ConsumerWidget {
const StatsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final statsAsync = ref.watch(statsProvider);
return Scaffold(
appBar: AppBar(title: const Text('My Stats')),
body: statsAsync.when(
data: (stats) => Center(child: Text('Watched: ${stats.totalEpisodesWatched}')),
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(child: Text('Error: $err')),
),
);
}
}Step 5: Register in GoRouter
Add the screen definition to lib/core/router/app_router.dart:
dart
GoRoute(
path: '/stats',
name: 'stats',
builder: (context, state) => const StatsScreen(),
),2. Modifying Remote Configuration Flags
ShonenX uses a remote configuration model to manage feature flags, override API configurations, or handle urgent updates dynamically.
- Open
lib/core/remote_config/models/remote_config.dart. - Add your field to the Freezed model:dart
@freezed class RemoteConfig with _$RemoteConfig { const factory RemoteConfig({ // ... existing configuration fields @Default(false) bool enableExperimentalPlayer, }) = _RemoteConfig; } - Run the code generator:bash
dart run build_runner build -d - Consume the configuration flag in your feature:dart
final remoteConfig = ref.watch(remoteConfigProvider); if (remoteConfig.value?.enableExperimentalPlayer == true) { // Run experimental feature }
3. Adding an Episode Metadata Provider
To add a new service for fetching episode thumbnails, descriptions, and titles (e.g. Jikan, Tenrai):
- Navigate to
lib/features/episode_metadata/services/. - Create
my_service_metadata_client.dart. - Implement the
EpisodeMetadataProviderinterface:resolveId(UnifiedMedia media): Maps universal media metadata to the provider's specific ID.fetchEpisodes(String serviceId): Returns structured episode metadata.
- Register the new client in
lib/features/episode_metadata/providers/episode_metadata_providers.dart. - The UI automatically uses it as a fallback if the primary metadata provider does not have data for an entry.