Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# LLM Agent Instructions - Shikkanime Application (Flutter)

As an AI agent, your primary directive is to adhere to the established patterns and architectural principles of this project. Your goal is to write clean, maintainable Flutter code that aligns with the existing codebase. This application is built on a **Model-View-ViewModel (MVVM)** architecture and is currently being reworked on the `v2` branch.

This file contains your core, non-negotiable rules. For detailed implementation guidance, refer to the linked documents in the `guidelines` directory.

## 1. The Prime Directive: Respect the MVVM Architecture

The project follows a strict MVVM layered architecture. **Do not violate this structure.**

```
View (widgets) -> ViewModel (ChangeNotifier) -> Repository -> HttpClient -> API
```

- **Views** (`lib/views/`) are "dumb" widgets. They display state and forward user events. They must NOT contain business logic.
- **ViewModels** (`lib/viewmodels/`) hold UI state and logic, extend `ChangeNotifier`, and expose the state the view reads.
- **Repositories** (`lib/repositories/`) are the single source of truth for data and orchestrate HTTP calls. They are `const` classes with constructor injection.
- **Models** (`lib/models/`) are immutable DTOs, mostly generated with `json_serializable`.
- Data flows in **one direction**: UI reads state, sends events to the ViewModel, the ViewModel calls the Repository, then notifies the UI.

For a deeper understanding, read the [Architecture Guide](guidelines/ARCHITECTURE.md).

The UI layer MUST use `context.read<T>()` to trigger events and `context.watch<T>()` to read reactive state. **No global singletons.**

## 2. State Management: Provider + ChangeNotifier

- Dependency injection is handled with the `provider` package. Wire everything in `main.dart` through `MultiProvider`.
- ViewModels extend `ChangeNotifier`; call `notifyListeners()` after mutating state that the UI observes.
- **Do not put logic in widgets.** Widgets only bind to the ViewModel.
- Read the [State Management Guide](guidelines/STATE_MANAGEMENT.md) for details.

## 3. Write Code for Humans (and other AIs)

- Write all code, comments, and documentation in **English**.
- Your code must be readable and self-explanatory. Add comments only when the logic is non-obvious.
- **Reuse existing patterns and conventions.** Before writing new code, understand how similar features are implemented in the project.
- Keep one responsibility per class and function, keep functions short, and do not add premature abstraction.

Refer to the style and convention guides for specifics:
- [Code Style Guide](guidelines/CODE_STYLE.md)
- [Models Guide](guidelines/MODELS.md)
- [API Conventions](guidelines/API_CONVENTIONS.md)
- [Theme Guide](guidelines/THEME.md)

## 4. Security is Not Optional

- Treat **all** external input (responses, query params, user input) as untrusted.
- Validate and sanitize incoming data before use.
- **Never** log sensitive information such as tokens, cookies, or personal identifiers.
- Consult the [Security Guide](guidelines/SECURITY.md) for detailed instructions.

## 5. Testing and Localization

- Tests are written with `Given / When / Then` structure using fakes (subclassing concrete classes).
- UI text goes through `AppLocalizations`; source language is French (`app_fr.arb`).
- Consult the [Testing Guide](guidelines/TESTING.md) and [Localization Guide](guidelines/LOCALIZATION.md).

## 6. Before Submitting Changes

Before concluding your task, ensure:
- The change fits the correct architectural layer (View/ViewModel/Repository/Model).
- Existing patterns and conventions are reused.
- `dart format lib` and `dart analyze lib` pass cleanly.
- No sensitive data is logged.
- Reusable widgets go to `lib/core/widgets/`, not ad-hoc.

Consult the [Performance Guide](guidelines/PERFORMANCE.md) for rules as needed.
102 changes: 102 additions & 0 deletions guidelines/API_CONVENTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# API Conventions

Networking is centralized in the `core/network/` layer: a single `HttpClient` and a `sealed class ApiResult<T>`. Repositories build endpoints and map responses.

## ApiResult<T>

Every request returns an `ApiResult<T>`:

- `ApiSuccess<T>(data)` — HTTP 2xx and payload parsed.
- `ApiFailure<T>([error, statusCode])` — non-2xx status or a parse/network error.

```dart
sealed class ApiResult<T> {
const ApiResult();
}

class ApiSuccess<T> extends ApiResult<T> {
const ApiSuccess(this.data);
final T data;
}

class ApiFailure<T> extends ApiResult<T> {
const ApiFailure([this.error, this.statusCode]);
final String? error;
final int? statusCode;
}
```

Because `ApiResult` is `sealed`, consumers handle it with an exhaustive `switch`.

## HttpClient

A single `const HttpClient` wraps `package:http`, builds the URI from `EnvConfig.apiBaseUrl`, enforces a `timeout`, checks the status, and parses JSON.

- Call it as `get<T>(endpoint, queryParameters: {...})`.
- `T` is the **raw parsed type** (`Map<String, dynamic>` or `List<dynamic>`), not the model. JSON is decoded to `Map`/`List`, and `_parseJson` returns `ApiFailure` when the decoded value is not of type `T`.

```dart
final response = await _httpClient.get<Map<String, dynamic>>(
'v1/animes',
queryParameters: {
'country': 'FR',
if (query != null && query.isNotEmpty) 'name': query,
'page': page,
'limit': limit,
},
);
```

## Repository mapping pattern

Repositories are `const` classes holding an `HttpClient` via constructor injection. They:

1. call `_httpClient.get<T>(...)` with an appropriate raw type (`Map<String, dynamic>` for one object, `List<dynamic>` for a list);
2. `switch` over the returned `ApiResult`;
3. map the raw payload (or the raw list) to typed models through `Model.fromJson(...)`.

```dart
class WeeklyRepository {
const WeeklyRepository(this._httpClient);

final HttpClient _httpClient;

Future<ApiResult<List<WeeklyDayModel>>> getWeekly({
List<LangType>? langTypes,
}) async {
final response = await _httpClient.get<List<dynamic>>(
'v1/animes/weekly',
queryParameters: {
'country': 'FR',
if (langTypes != null && langTypes.isNotEmpty)
'searchTypes': langTypes.map((e) => e.name.toUpperCase()).join(','),
},
);

return switch (response) {
ApiSuccess<List<dynamic>>(:final data) =>
ApiSuccess<List<WeeklyDayModel>>(
data.map((e) => WeeklyDayModel.fromJson(e as Map<String, dynamic>))
.toList(),
),
ApiFailure<List<dynamic>> failure => ApiFailure<List<WeeklyDayModel>>(
failure.error,
failure.statusCode,
),
};
}
}
```

## Query parameters

- Add query parameters conditionally with collection-`if` so empty/null values are omitted.
- Enums in query params are serialized via `e.name.toUpperCase()` (matching the API contract).
- Always include required API params (e.g. `'country': 'FR'`).

## Guidelines

- Repositories are the **only** layer that talks to `HttpClient`. ViewModels never call `HttpClient` directly.
- Keep methods compact and map results at the repository boundary.
- Endpoints are versioned (`v1/...`); keep them aligned with the API.
- 🌱 To consider for the rework: Flutter officially recommends abstract repository classes for per-environment implementations and separate API vs domain models in large apps. Not required now; concrete `const` repositories are the documented norm.
72 changes: 72 additions & 0 deletions guidelines/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Architecture Guide

The application follows a **Model-View-ViewModel (MVVM)** architecture. Respect the project layers and their dependency direction:

```
View (widgets) -> ViewModel (ChangeNotifier) -> Repository -> HttpClient -> API
```

## Layer responsibilities

### Views (`lib/views/`)
- Widgets are **"dumb"**: they render state and forward user events to the ViewModel.
- The only logic a view may contain is:
- simple `if` conditions that show/hide a widget based on a flag or nullable field on the ViewModel;
- layout logic based on device information (screen size, orientation);
- simple routing logic.
- Views read state with `context.watch<T>()` and trigger events with `context.read<T>()`.

### ViewModels (lib/viewmodels/)
- Hold UI state and the logic that operates on it.
- Extend `ChangeNotifier` (via `package:flutter/foundation.dart`) and call `notifyListeners()` after any mutation the UI observes.
- Orchestrate calls to Repositories; they never perform HTTP or layout work directly.
- Compose other ViewModels by constructor injection (see `AnimeViewModel` which depends on `SimulcastViewModel`).

### Repositories (lib/repositories/)
- **Single source of truth for data.** Repositories isolate the app from the API.
- `const` classes whose constructor receives an `HttpClient`.
- They build endpoints, attach query parameters, execute the HTTP call, and map the raw response to typed models (see the [API Conventions](API_CONVENTIONS.md) for the exact mapping pattern).

### Models (lib/models/)
- Immutable DTOs. Use `final` fields and constructor injection (`AnimeModel(this.uuid, ...)`).
- Marked with `@JsonSerializable`; generated factory logic lives in the sibling `.g.dart` file (`part 'anime_model.g.dart'`).
- See the [Models Guide](MODELS.md).

### Core (lib/core/)
- Shared, reusable code across the app: configuration (`config/`), networking (`network/`), theming (`theme/`), and shared widgets (`widgets/`).
- Reusable widgets live in `lib/core/widgets/`, not ad-hoc inside a screen folder.

## Navigation

The app uses a single `PageView` + `BottomNavigationBar` driven by `NavigationViewModel`. There is no URL router. Use the existing pattern for the app.

- 🌱 To consider for the rework: Flutter officially recommends `go_router` for navigation in most apps. Not required yet; document the existing `PageView` pattern as the norm.

## Repository and models: to consider

- 🌱 Flutter strongly recommends abstract repository classes so different environments (dev/staging) can swap implementations, and generated immutable models (`freezed` / `built_value`). These are **not** required now; the existing concrete repositories (`const` + constructor injection) and `json_serializable` models are the documented norm.

## Project structure (v2)

```
lib/
main.dart # Composition root: MultiProvider + runApp + MaterialApp
core/
config/ # EnvConfig (env-driven values)
network/ # HttpClient, ApiResult<T>
theme/ # AppTheme, AppThemeExtension, app_theme_colors
widgets/ # Shared, reusable widgets (app_*, lang_types/*)
models/ # Immutable DTOs + generated .g.dart
repositories/ # Concrete repositories (source of truth for data)
viewmodels/ # ChangeNotifier view models
views/ # Screen widgets + per-screen widgets/
l10n/ # Generated localizations (app_localizations, *_fr)
```

`lib/data/` (repositories + services) exists but is **empty**; it is a leftover of the rework in progress. Do not add code there; `lib/repositories/` is the active location.

## Architecture principles (transversal)

- **Single responsibility**: one concern per class/function.
- **No dead or commented-out code.**
- **Unidirectional data flow**: interact from UI to data layer; updates flow back through `ChangeNotifier`.
87 changes: 87 additions & 0 deletions guidelines/CODE_STYLE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Code Style Guide

## General style

- **Write all code, examples, and comments in English.**
- **Prefer short, readable, direct code** with explicit, descriptive names.
- **One responsibility per class and function.** Keep functions short and focused.
- **One statement per line.** Do not chain multiple statements on a single line.
- **No visual separators** in comments (no ASCII banner lines, no decorative `#`, `-`, `*` runs).
- **Reuse existing project patterns** instead of introducing new ones without need.
- **No premature abstraction and no duplication (DRY).** Do not copy-paste logic; extract it when reused.

## Classes, constructors and initializers

- **Use `const` constructors** (and `const` class instances) whenever possible. The `flutter_lints` analyzer enforces this.
- **Prefer `const` declarations** for local values and class members that never change.
- **Prefer constructors over static factory methods** unless the factory genuinely shines (this is enforced by the analyzer).
- **Prefer constructor injection** over positional parameter lists that are too long; use named optional parameters with defaults where needed.

```dart
class HttpClient {
const HttpClient({this.timeout = const Duration(seconds: 10)});

final Duration timeout;
}
```

For immutable data classes, use compact generative constructors:

```dart
class AnimeModel {
final String uuid;
final String shortName;
final List<LangType> langTypes;
final List<AnimePlatformModel> platformIds;

AnimeModel(this.uuid, this.shortName, this.langTypes, this.platformIds);
}
```

## Imports

- **Import symbols directly** (`import 'package:application/models/lang_type.dart';`); never reference a type by its fully-qualified name inline.
- Imports are a single statement per line, following the existing ordering (project imports first, then third-party).

## Expression bodies

- Prefer **expression bodies** (`=>`) for simple getters and short functions, matching the existing code.

```dart
int get length => _animes.length + (_loading ? 8 : 0);

AnimeModel? getOrNull(int index) =>
index >= 0 && index < _animes.length ? _animes[index] : null;
```

## No singletons

- **No global singletons or `Static` global state.** Dependencies are provided through the `provider` package (`MultiProvider`) in `main.dart` and injected via constructors.
- In widgets, read state with `context.watch<T>()` and send events with `context.read<T>()`.

## Naming

- Classes and interfaces: `PascalCase` (`AnimeViewModel`, `AppHorizontalListView`).
- Files and directories: `snake_case.dart` (`anime_view_model.dart`, `lang_type.dart`).
- Private instance fields: leading underscore (`_animes`, `_repository`).
- Local variables, parameters, methods, getters: `camelCase`.
- Constants: private-ish `camelCase` prefixed with `_` for instance values; `const` for true constants.
- Public domain interfaces (shared across layers) use `VerbNounViewModel` / `NounViewModel` naming (e.g. `LangTypeFilterViewModel`).

## Flutter / Dart conventions

- Widgets that hold state define a private `_State` companion class (`CatalogView` -> `_CatalogViewState`).
- Use `const` widget subtrees (`const Icon(...)`) whenever the subtree is static.
- Use `switch` expressions and modern Dart constructs (`is!`, `runtimeType`, spread) over legacy imperative equivalents where they read more cleanly.

## Lints (enforced)

The repository's `analysis_options.yaml` enables `flutter_lints` (`flutter_lints: ^6.0.0`) with these rules. Code MUST satisfy the analyzer at review time. The most relevant enforced rules:

- `prefer_const_constructors`
- `prefer_const_constructors_in_immutables`
- `prefer_const_declarations`
- `prefer_const_literals_to_create_immutables`
- `prefer_constructors_over_static_methods`

Always run `dart format lib` and `dart analyze lib` before submitting. `lib/models/*.g.dart` is excluded from analysis (generated code).
Loading
Loading