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
176 changes: 65 additions & 111 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,136 +1,90 @@
# AGENTS.md

`[opencode] AGENTS.md loaded`
## Project

## Project Overview
TodoApp — sample Flutter app (Android/iOS/macOS) for checklists + tasks,
persisted with sqflite. flutter_bloc (Cubit), get_it + injectable, auto_route,
freezed, EN/PT localization. Dart package name is `todoapp`.

TodoApp is a sample Flutter project built as a testing playground. It manages
checklists and tasks using sqflite for local persistence. Targets Android, iOS,
and macOS. Uses flutter_bloc (Cubit) for state management, get_it + injectable
for DI, auto_route for navigation, freezed for immutable models, and supports
English/Portuguese localization.
## Commands

## Dev Environment Setup

1. Install Flutter 3.38.x (see `.github/actions/setup-flutter/action.yml` for
the pinned version)
2. `flutter pub get`
3. `dart run build_runner build --delete-conflicting-outputs`
4. `flutter gen-l10n`

Steps 3 and 4 must be re-run after any changes to annotated classes
(`*.freezed.dart`, router config, injectable config) or `.arb` localization
files.
```sh
flutter pub get
dart run build_runner build --delete-conflicting-outputs # freezed/injectable/auto_route
flutter gen-l10n # lib/ui/l10n/app_localizations*.dart
flutter test
flutter test test/domain/tasks_list_sort_test.dart # one file
flutter test --plain-name 'TasksViewModel' # one test
dart analyze
dart run eagle_eye:main # architecture check
flutter run -d <device>
```

## Project Architecture
Flutter is pinned to 3.38.3 (`.github/actions/setup-flutter/action.yml`).
Re-run codegen after touching `@freezed` classes, `@Injectable`/DI, `@RoutePage`
/router config, or `.arb` files — tests and `dart analyze` fail without the
generated files. Generated outputs (`*.freezed.dart`, `*.config.dart`,
`*.gr.dart`, `lib/ui/l10n/app_localizations*.dart`) are gitignored; never
hand-edit or commit them.

### Layer-first layout
## Layout

```
lib/
data/ — models (freezed), DAOs (sqflite), repository, share handler
domain/ — sort & summary helpers
ui/ — screens (features), shared components, widgets, l10n
util/ — DI (get_it + injectable), navigation provider
domain/ model/ (freezed) + repository interface + sort/summary helpers
data/ database/ DAOs, todo_repository_impl.dart, share_message_handler.dart
ui/ screens/<feature>/ (screen + viewmodel + screen_state),
components/widgets, l10n/
util/ di/ (injectable), navigation_provider.dart
```

### Key patterns

- **State management:** flutter_bloc (Cubit) with freezed immutable states.
Cubits are `@Injectable()` and used via `BlocProvider`/`BlocBuilder`.
- **DI:** get_it + injectable with code-gen. Initialized at startup via
`GetItStartupHandlerWrapper`.
- **Navigation:** auto_route (generated router). Abstracted behind
`NavigatorProvider` interface for testability.
- **Data:** Abstract repository → Impl → DAO pattern. The Abstract + Impl
convention is also used for `NavigatorProvider`, `ShareMessageHandler`, and
`TaskListSortHelper`.

### Code generation
`main.dart` only runs `TodoApp`; DI and the router initialize in
`lib/ui/screens/startup/startup_screen.dart` via
`GetItStartupHandlerWrapper.init()`.

Generated files (`*.freezed.dart`, `*.config.dart`, `*.gr.dart`,
`app_localizations*.dart`) are gitignored. Generated on demand via
`build_runner`.
## Architecture constraints (eagle_eye, enforced in CI)

## Coding Conventions
Config: `eagle_eye_config.json`. It only checks internal
(`package:todoapp/...`) imports; external packages are always allowed.

### Naming
- `*viewmodel.dart` may only import `package:todoapp/domain/*` and
`*_screen_state.dart`. Keep UI/widgets out of viewmodels.
- `lib/util/*_provider.dart` and `lib/util/*_handler.dart` may import no other
`package:todoapp/*` file.
- The `*/data/model/*` rule matches nothing today (models live in
`lib/domain/model/`).

- Files: `snake_case` (enforced by `file_names` lint)
- Classes/widgets: `PascalCase`
- Methods/variables: `camelCase`
- Abstract interfaces: plain name (e.g. `TodoRepository`); implementation:
name + `Impl`
## Conventions

### Imports
Lints are promoted to `error` in `analysis_options.yaml`, so violations fail
`dart analyze`. Highest impact: single quotes, `package:` imports only (no
relative), directive ordering (`dart:` → `package:` → project), 80-char lines,
explicit return types, `const` constructors, braces on all control flow,
snake_case files. `avoid_print` is disabled.

- Always use package imports (`package:todoapp/...`), never relative (enforced
by `always_use_package_imports` and `avoid_relative_lib_imports`)
- Directives ordered as: `dart:` → `package:` → project (enforced by
`directives_ordering`)

### Style

- Single quotes (enforced)
- `const` constructors wherever possible (enforced)
- Lines max 80 chars (enforced by `lines_longer_than_80_chars`)
- Curly braces required on all flow control (enforced by
`curly_braces_in_flow_control_structures`)
- Always declare return types (enforced by `always_declare_return_types`)

### Architecture rules (eagle_eye)

- `data/model/*` must have zero external dependencies
- `*viewmodel.dart` must not depend on `*_screen.dart`
- `util/*_provider.dart` and `util/*_handler.dart` must have zero external
dependencies
Naming: interfaces plain (`TodoRepository`), implementations `...Impl`. Cubits
are `@Injectable()`; other impls `@Injectable(as: Interface)`.

## Testing

### Running tests

```
flutter test
```

Code generation (`build_runner` + `flutter gen-l10n`) must complete before
tests pass.

### Organization

Tests mirror `lib/` structure under `test/` (layer-first).

### Patterns

- **Domain/logic tests:** Pure Dart, no widget testing. Use
Arrange/Act/Assert with real implementations (no mocking framework).
- **ViewModel (Cubit) tests:** Use `FakeRepository` (in-memory) from
`test/test_utils/fakes/`. Test initial state, then state transitions after
method calls.
- **Widget tests:** Wrap with `WidgetsUtil.buildMaterialAppWidgetTest()`
(sets up MaterialApp + localization delegates, forced English locale). Use
`testWidgets()`, `pumpWidget()`, `find.byKey()`, `find.text()`.

### Test doubles

Hand-written fakes in `test/test_utils/fakes/` (no mockito):
`FakeRepository`, `FakeNavigatorProvider`, `FakeCallbacks`, `FakeStates`.

## PR & CI

### PR checks (`.github/workflows/pr.yml`)

Triggered on PRs to `main`. Runs on every PR:
- Codegen must complete before tests (localizations are required).
- Mirror `lib/` under `test/`; domain logic is plain Dart
(Arrange/Act/Assert, real implementations).
- Cubit tests use the in-memory `FakeRepository` from
`test/test_utils/fakes/`. No mocking framework — fakes are hand-written
(`FakeRepository`, `FakeNavigatorProvider`, `FakeCallbacks`, `FakeStates`).
- Widget tests wrap with `WidgetsUtil.buildMaterialAppWidgetTest(tester: tester,
child: ...)`; it forces the `en` locale because tests assert English strings.

1. `flutter gen-l10n` — generate localizations
2. `dart run eagle_eye:main` — check architecture violations
3. `flutter test` — run test suite
4. `dart analyze` — static analysis
5. `flutter build apk --debug` — verify build compiles
## CI / release

All steps must pass before merging.
PRs to `main` (`.github/workflows/pr.yml`), all must pass:
`flutter gen-l10n` → `dart run eagle_eye:main` → `flutter test` →
`dart analyze` → `flutter build apk --debug`.

### Release (`.github/workflows/release_flutter_app.yaml`)
Any tag triggers `.github/workflows/release_flutter_app.yaml` (APK to Firebase
App Distribution, macOS zip to a GitHub Release).

Triggered on any tag push. Builds APK (uploaded to Firebase App Distribution)
and macOS `.app.zip` (attached to GitHub Release).
Commits follow conventional commits; the repo-local `feature-branch-pr` skill
(`.opencode/skills/feature-branch-pr/SKILL.md`) creates `feature/<name>`
branches and PRs.
4 changes: 4 additions & 0 deletions android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
2 changes: 1 addition & 1 deletion eagle_eye_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
},
{
"filePattern": "*viewmodel.dart",
"forbiddenDependencies": ["*_screen.dart"]
"exclusiveDependencies": ["*/domain/*", "*_screen_state.dart"]
},
{
"filePattern": "*/util/*_provider.dart",
Expand Down
2 changes: 1 addition & 1 deletion lib/data/database/checklist_dao.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:injectable/injectable.dart';
import 'package:sqflite/sqflite.dart';
import 'package:todoapp/data/database/task_dao.dart';
import 'package:todoapp/data/model/checklist.dart';
import 'package:todoapp/domain/model/checklist.dart';

@LazySingleton()
class ChecklistDAO {
Expand Down
2 changes: 1 addition & 1 deletion lib/data/database/task_dao.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:injectable/injectable.dart';
import 'package:sqflite/sqflite.dart';
import 'package:todoapp/data/database/checklist_dao.dart';
import 'package:todoapp/data/model/task.dart';
import 'package:todoapp/domain/model/task.dart';

@LazySingleton()
class TaskDAO {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,10 @@
import 'package:injectable/injectable.dart';
import 'package:todoapp/data/database/checklist_dao.dart';
import 'package:todoapp/data/database/task_dao.dart';
import 'package:todoapp/data/model/checklist.dart';
import 'package:todoapp/data/model/task.dart';
import 'package:todoapp/data/share_message_handler.dart';

abstract class TodoRepository {
Future<List<Task>> getTasks(int? checklistId);

Future<bool> updateTask(Task task, bool isCompletedNewValue);

Future<bool> updateTasks(List<Task> tasks, bool isCompletedNewValue);

Future<bool> addTask(Task task, int? checklistId);

Future<bool> deleteTasks(List<Task> tasks);

Future<void> updateAllTasks(List<Task> tasks);

Future<bool> addChecklist(Checklist checklist);

Future<bool> deleteChecklist(Checklist checklist);

Future<List<Checklist>> getChecklists();

Future<bool> updateTaskName({
required int checklistId,
required int taskId,
required String taskTitle,
});

Future<bool> share({
required String text,
required String title,
});
}
import 'package:todoapp/domain/model/checklist.dart';
import 'package:todoapp/domain/model/task.dart';
import 'package:todoapp/domain/todo_repository.dart';

@Injectable(as: TodoRepository)
class TodoRepositoryImpl implements TodoRepository {
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion lib/domain/task_list_sort_helper.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:injectable/injectable.dart';
import 'package:todoapp/data/model/task.dart';
import 'package:todoapp/domain/model/task.dart';

abstract class TaskListSortHelper {
bool areThemEqual({required List<Task> oldList, required List<Task> newList});
Expand Down
4 changes: 2 additions & 2 deletions lib/domain/task_list_summary_helper.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:injectable/injectable.dart';
import 'package:todoapp/data/model/task.dart';
import 'package:todoapp/data/model/tasks_complete_status.dart';
import 'package:todoapp/domain/model/task.dart';
import 'package:todoapp/domain/model/tasks_complete_status.dart';

abstract class TaskListSummaryHelper {
bool shouldShowShareButton({required List<Task> tasks});
Expand Down
34 changes: 34 additions & 0 deletions lib/domain/todo_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

import 'package:todoapp/domain/model/checklist.dart';
import 'package:todoapp/domain/model/task.dart';

abstract class TodoRepository {
Future<List<Task>> getTasks(int? checklistId);

Future<bool> updateTask(Task task, bool isCompletedNewValue);

Future<bool> updateTasks(List<Task> tasks, bool isCompletedNewValue);

Future<bool> addTask(Task task, int? checklistId);

Future<bool> deleteTasks(List<Task> tasks);

Future<void> updateAllTasks(List<Task> tasks);

Future<bool> addChecklist(Checklist checklist);

Future<bool> deleteChecklist(Checklist checklist);

Future<List<Checklist>> getChecklists();

Future<bool> updateTaskName({
required int checklistId,
required int taskId,
required String taskTitle,
});

Future<bool> share({
required String text,
required String title,
});
}
4 changes: 2 additions & 2 deletions lib/ui/components/tasks_view_model/tasks_screen_state.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:todoapp/data/model/task.dart';
import 'package:todoapp/data/model/tasks_complete_status.dart';
import 'package:todoapp/domain/model/task.dart';
import 'package:todoapp/domain/model/tasks_complete_status.dart';

part 'tasks_screen_state.freezed.dart';

Expand Down
6 changes: 3 additions & 3 deletions lib/ui/components/tasks_view_model/tasks_viewmodel.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:todoapp/data/model/task.dart';
import 'package:todoapp/data/model/tasks_complete_status.dart';
import 'package:todoapp/data/todo_repository.dart';
import 'package:todoapp/domain/model/task.dart';
import 'package:todoapp/domain/model/tasks_complete_status.dart';
import 'package:todoapp/domain/task_list_sort_helper.dart';
import 'package:todoapp/domain/task_list_summary_helper.dart';
import 'package:todoapp/domain/todo_repository.dart';
import 'package:todoapp/ui/components/tasks_view_model/tasks_screen_state.dart';

@Injectable()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:todoapp/data/model/tasks_complete_status.dart';
import 'package:todoapp/domain/model/tasks_complete_status.dart';
import 'package:todoapp/ui/l10n/app_localizations.dart';

class CheckAllActionChipWidget extends StatelessWidget {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:todoapp/data/model/checklist.dart';
import 'package:todoapp/data/model/task.dart';
import 'package:todoapp/data/model/tasks_complete_status.dart';
import 'package:todoapp/domain/model/checklist.dart';
import 'package:todoapp/domain/model/task.dart';
import 'package:todoapp/domain/model/tasks_complete_status.dart';
import 'package:todoapp/ui/components/remove_task_dialog_builder.dart';
import 'package:todoapp/ui/components/tasks_view_model/tasks_viewmodel.dart';
import 'package:todoapp/ui/components/widgets/checklist/checklist_item_widget.dart';
Expand Down Expand Up @@ -111,7 +111,7 @@ class ChecklistsListFullWidgetState extends State<ChecklistsListFullWidget> {
context,
task,
),
onReorder: _tasksViewModel.reorder,
onReorderItem: _tasksViewModel.reorder,
onTap: (task) => _navigateToTaskScreen(
context,
checklistId: selected?.id,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';

import 'package:todoapp/data/model/checklist.dart';
import 'package:todoapp/domain/model/checklist.dart';
import 'package:todoapp/ui/components/widgets/card_wrapper_widget.dart';

class ChecklistItemWidget extends StatelessWidget {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:todoapp/data/model/checklist.dart';
import 'package:todoapp/domain/model/checklist.dart';
import 'package:todoapp/ui/components/widgets/checklist/checklist_item_widget.dart';

class ChecklistsListWidget extends StatelessWidget {
Expand Down
Loading
Loading