Architecture

MiyoList uses Clean Architecture principles with clear separation of concerns

Architecture Overview

MiyoList follows Clean Architecture with three main layers:

  • Presentation Layer: UI components, state management (Riverpod)
  • Domain Layer: Business logic, use cases, entities
  • Data Layer: API clients, local storage, repositories

Project Structure

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

Feature Module Structure

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

State Management

MiyoList uses Riverpod for state management:

Provider Example

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();
    });
  }
}

Using Providers in UI

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'),
    );
  }
}

Data Flow

Understanding how data moves through the app:

  1. 1.
    UI triggers action (e.g., button press)
  2. 2.
    Provider calls use case from domain layer
  3. 3.
    Use case executes business logic
  4. 4.
    Repository fetches/saves data via data sources
  5. 5.
    Data flows back through use case to provider
  6. 6.
    UI rebuilds with new state

3-Tier Caching Strategy

MiyoList implements a sophisticated caching system:

1

Memory

In-memory cache for instant access

  • • Fastest access
  • • Lost on app restart
  • • Limited capacity
2

Disk

Persistent local storage

  • • Survives restarts
  • • Larger capacity
  • • Offline access
3

Network

Fresh data from AniList API

  • • Always up-to-date
  • • Requires internet
  • • Rate limited

Key Services

AuthService

Handles user authentication, token management, and session persistence

ApiClient

GraphQL client for AniList API with automatic retries and rate limiting

StorageService

Manages local storage using SharedPreferences and path_provider

CrashReportingService

Captures and logs crashes/errors for debugging (Sentry integration)

UpdateService

Checks for app updates and manages update process

Best Practices

✅ Do:

  • • Keep business logic in domain layer
  • • Use dependency injection via Riverpod
  • • Handle errors gracefully with AsyncValue
  • • Write unit tests for use cases
  • • Follow existing naming conventions

❌ Don't:

  • • Mix UI logic with business logic
  • • Call API directly from widgets
  • • Store sensitive data in plaintext
  • • Ignore rate limits
  • • Skip error handling