MiyoList uses Clean Architecture principles with clear separation of concerns
MiyoList follows Clean Architecture with three main layers:
lib/
├── core/ # Shared utilities and constants
│ ├── constants/ # App-wide constants
│ ├── services/ # Core services (auth, storage, etc.)
│ ├── theme/ # Theme configuration
│ └── utils/ # Helper functions
│
├── features/ # Feature modules
│ ├── anime_list/ # Anime list management
│ │ ├── data/ # Data sources, models
│ │ ├── domain/ # Entities, use cases
│ │ └── presentation/ # UI, providers
│ │
│ ├── search/ # Global search
│ ├── activity/ # Activity feed
│ ├── profile/ # User profile
│ └── statistics/ # Stats and analytics
│
└── main.dart # App entry point Each feature follows a consistent structure:
feature_name/
├── data/
│ ├── models/ # Data models (JSON serializable)
│ │ └── anime_model.dart
│ ├── repositories/ # Repository implementations
│ │ └── anime_repository_impl.dart
│ └── data_sources/ # API clients, local DB
│ ├── anime_remote_data_source.dart
│ └── anime_local_data_source.dart
│
├── domain/
│ ├── entities/ # Business entities
│ │ └── anime.dart
│ ├── repositories/ # Repository interfaces
│ │ └── anime_repository.dart
│ └── usecases/ # Business logic
│ └── get_anime_list.dart
│
└── presentation/
├── providers/ # Riverpod providers
│ └── anime_list_provider.dart
├── pages/ # Full screen pages
│ └── anime_list_page.dart
└── widgets/ # Reusable widgets
└── anime_card.dart MiyoList uses Riverpod for state management:
final animeListProvider =
StateNotifierProvider<AnimeListNotifier, AsyncValue<List<Anime>>>((ref) {
return AnimeListNotifier(ref.watch(animeRepositoryProvider));
});
class AnimeListNotifier extends StateNotifier<AsyncValue<List<Anime>>> {
AnimeListNotifier(this._repository) : super(const AsyncValue.loading());
final AnimeRepository _repository;
Future<void> fetchAnimeList() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return await _repository.getAnimeList();
});
}
} class AnimeListPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final animeListState = ref.watch(animeListProvider);
return animeListState.when(
data: (animeList) => ListView.builder(...),
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
);
}
} Understanding how data moves through the app:
MiyoList implements a sophisticated caching system:
In-memory cache for instant access
Persistent local storage
Fresh data from AniList API
Handles user authentication, token management, and session persistence
GraphQL client for AniList API with automatic retries and rate limiting
Manages local storage using SharedPreferences and path_provider
Captures and logs crashes/errors for debugging (Sentry integration)
Checks for app updates and manages update process