|
| 1 | +# OnTime Flutter Project - AI Coding Agent Instructions |
| 2 | + |
| 3 | +## Architecture Overview |
| 4 | + |
| 5 | +This is a **Flutter app** following **Clean Architecture** with three layers: |
| 6 | + |
| 7 | +- **Presentation** (`lib/presentation/`): Screens, widgets, BLoC/Cubit state management |
| 8 | +- **Domain** (`lib/domain/`): Business logic, entities, use cases, repository interfaces |
| 9 | +- **Data** (`lib/data/`): Repository implementations, data sources (remote/local), DAOs, database tables |
| 10 | + |
| 11 | +Data flows: UI → BLoC → UseCase → Repository → DataSource → API/DB |
| 12 | + |
| 13 | +## Critical Build Commands |
| 14 | + |
| 15 | +```bash |
| 16 | +# ALWAYS run after pulling or modifying data models, entities, or DI |
| 17 | +dart run build_runner build --delete-conflicting-outputs |
| 18 | + |
| 19 | +# Watch mode for development |
| 20 | +dart run build_runner watch |
| 21 | + |
| 22 | +# Run tests |
| 23 | +flutter test |
| 24 | + |
| 25 | +# Run app |
| 26 | +flutter run |
| 27 | +``` |
| 28 | + |
| 29 | +## Dependency Injection Pattern |
| 30 | + |
| 31 | +- Uses **Injectable** + **GetIt** for DI |
| 32 | +- All repositories, use cases, BLoCs, and data sources must be annotated: |
| 33 | + - `@Injectable()` for standard dependencies |
| 34 | + - `@Singleton()` for app-wide singletons (e.g., `ScheduleBloc`, `AppDatabase`) |
| 35 | + - `@LazySingleton()` for lazy-initialized singletons |
| 36 | +- After adding `@Injectable()`, run `dart run build_runner build` |
| 37 | +- Register as interface: `@Injectable(as: InterfaceName)` |
| 38 | + |
| 39 | +Example: |
| 40 | + |
| 41 | +```dart |
| 42 | +@Injectable() |
| 43 | +class UpdateScheduleUseCase { |
| 44 | + final ScheduleRepository _repository; |
| 45 | + UpdateScheduleUseCase(this._repository); |
| 46 | +} |
| 47 | +
|
| 48 | +@Singleton(as: ScheduleRepository) |
| 49 | +class ScheduleRepositoryImpl implements ScheduleRepository { ... } |
| 50 | +``` |
| 51 | + |
| 52 | +## State Management with BLoC |
| 53 | + |
| 54 | +- Use **BLoC** for complex state, **Cubit** for simpler scenarios |
| 55 | +- BLoCs live in `lib/presentation/*/bloc/` directories |
| 56 | +- Pattern: `on<EventName>(_onEventName)` event handlers |
| 57 | +- **Critical**: `ScheduleBloc` is a `@Singleton()` that manages automatic timer system for schedule start notifications (see `docs/Schedule-Timer-System.md`) |
| 58 | +- Keep business logic in **use cases**, not BLoCs |
| 59 | +- BLoCs coordinate between use cases and UI |
| 60 | + |
| 61 | +Example structure: |
| 62 | + |
| 63 | +```dart |
| 64 | +@Injectable() |
| 65 | +class MyBloc extends Bloc<MyEvent, MyState> { |
| 66 | + MyBloc(this._useCase) : super(MyState.initial()) { |
| 67 | + on<MyEventHappened>(_onMyEventHappened); |
| 68 | + } |
| 69 | +
|
| 70 | + Future<void> _onMyEventHappened(MyEventHappened event, Emitter<MyState> emit) async { |
| 71 | + final result = await _useCase(); |
| 72 | + emit(state.copyWith(data: result)); |
| 73 | + } |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +## Data Layer Patterns |
| 78 | + |
| 79 | +### Entities (Domain) |
| 80 | + |
| 81 | +- Use **Equatable** for value equality (most entities) |
| 82 | +- Or **Freezed** for advanced immutability (e.g., `UserEntity`) |
| 83 | +- Live in `lib/domain/entities/` |
| 84 | +- Example: `ScheduleEntity`, `PreparationEntity` |
| 85 | + |
| 86 | +### Data Models (Data Layer) |
| 87 | + |
| 88 | +- Use **json_serializable** for API models |
| 89 | +- Live in `lib/data/models/` |
| 90 | +- Must have `toEntity()` method to convert to domain entities |
| 91 | +- Repository boundary: models → entities when leaving data layer |
| 92 | + |
| 93 | +### Database (Drift) |
| 94 | + |
| 95 | +- Tables defined in `lib/data/tables/` |
| 96 | +- DAOs in `lib/data/daos/` |
| 97 | +- Database: `lib/core/database/database.dart` with `@Singleton()` annotation |
| 98 | +- Schema version tracked, migrations in `MigrationStrategy` |
| 99 | + |
| 100 | +### Data Sources |
| 101 | + |
| 102 | +- Abstract interface + implementation pattern |
| 103 | + // ...existing code... |
| 104 | + - Remote sources use Dio client (`lib/core/dio/app_dio.dart`) |
| 105 | + - Local sources use Drift DAOs or SharedPreferences |
| 106 | + - Example: `ScheduleRemoteDataSource` + `ScheduleRemoteDataSourceImpl` |
| 107 | + |
| 108 | +## Reactive Data Fetching Pattern |
| 109 | + |
| 110 | +The app uses a **reactive repository pattern** where UI updates automatically when data changes. |
| 111 | + |
| 112 | +1. **Repository**: Holds a `BehaviorSubject` (Stream) of data. |
| 113 | + |
| 114 | + - Methods like `getSchedulesByDate` fetch from API/DB and **add the result to the stream**. |
| 115 | + - `scheduleStream` exposes this subject as a broadcast stream. |
| 116 | + |
| 117 | +2. **UseCase**: Transforms the repository stream. |
| 118 | + |
| 119 | + - Can use `async*` to yield transformed states. |
| 120 | + - Combines multiple data sources if needed. |
| 121 | + |
| 122 | +3. **BLoC**: Subscribes to the UseCase stream. |
| 123 | + // ...existing code... |
| 124 | + - `listen()` to the stream in event handlers. |
| 125 | + - `add()` new events to the BLoC when stream emits. |
| 126 | + |
| 127 | +Example `ScheduleRepositoryImpl`: |
| 128 | + |
| 129 | +```dart |
| 130 | +@Singleton(as: ScheduleRepository) |
| 131 | +class ScheduleRepositoryImpl implements ScheduleRepository { |
| 132 | + late final _scheduleStreamController = BehaviorSubject<Set<ScheduleEntity>>.seeded({}); |
| 133 | +
|
| 134 | + @override |
| 135 | + Stream<Set<ScheduleEntity>> get scheduleStream => _scheduleStreamController.asBroadcastStream(); |
| 136 | +
|
| 137 | + @override |
| 138 | + Future<List<ScheduleEntity>> getSchedulesByDate(...) async { |
| 139 | + final schedules = await remoteDataSource.getSchedulesByDate(...); |
| 140 | + // Update the stream with new data |
| 141 | + _scheduleStreamController.add(Set.from(_scheduleStreamController.value)..addAll(schedules)); |
| 142 | + return schedules; |
| 143 | + } |
| 144 | +} |
| 145 | +``` |
| 146 | + |
| 147 | +## Form Management & Validation |
| 148 | + |
| 149 | +The app uses a **Multi-Step Form Pattern** (e.g., `ScheduleMultiPageForm`): |
| 150 | + |
| 151 | +1. **Parent BLoC**: `ScheduleFormBloc` manages the overall form state and final submission. |
| 152 | +2. **Step Cubits**: Each form step has its own Cubit (e.g., `ScheduleNameCubit`) that: |
| 153 | + - Manages local UI state for that step. |
| 154 | + - Communicates updates to the parent `ScheduleFormBloc`. |
| 155 | +3. **Validation**: |
| 156 | + - `ScheduleFormState` tracks `isValid` status. |
| 157 | + - `TopBar` enables navigation buttons based on validity. |
| 158 | + - `GlobalKey<FormState>` is used for per-page field validation. |
| 159 | + |
| 160 | +## Data Transfer & Navigation |
| 161 | + |
| 162 | +- **Route Arguments**: Pass IDs (e.g., `scheduleId`) via `GoRouter` path parameters or `extra` object. |
| 163 | +- **Screen Initialization**: |
| 164 | + - Screens receive arguments in their constructor. |
| 165 | + - BLoCs are initialized with these arguments (e.g., `ScheduleFormEditRequested(scheduleId)`). |
| 166 | + - BLoCs fetch full data from Repositories/UseCases using the ID. |
| 167 | + - **Avoid** passing full entity objects between screens; pass IDs and refetch/stream data. |
| 168 | + |
| 169 | +## Navigation (GoRouter) |
| 170 | + |
| 171 | +// ...existing code...Example `ScheduleRepositoryImpl`: |
| 172 | + |
| 173 | +```dart |
| 174 | +@Singleton(as: ScheduleRepository) |
| 175 | +class ScheduleRepositoryImpl implements ScheduleRepository { |
| 176 | + late final _scheduleStreamController = BehaviorSubject<Set<ScheduleEntity>>.seeded({}); |
| 177 | +
|
| 178 | + @override |
| 179 | + Stream<Set<ScheduleEntity>> get scheduleStream => _scheduleStreamController.asBroadcastStream(); |
| 180 | +
|
| 181 | + @override |
| 182 | + Future<List<ScheduleEntity>> getSchedulesByDate(...) async { |
| 183 | + final schedules = await remoteDataSource.getSchedulesByDate(...); |
| 184 | + // Update the stream with new data |
| 185 | + _scheduleStreamController.add(Set.from(_scheduleStreamController.value)..addAll(schedules)); |
| 186 | + return schedules; |
| 187 | + } |
| 188 | +} |
| 189 | +``` |
| 190 | + |
| 191 | +## Navigation (GoRouter) |
| 192 | + |
| 193 | +// ...existing code...## Navigation (GoRouter) |
| 194 | + |
| 195 | +- Configured in `lib/presentation/shared/router/go_router.dart` |
| 196 | +- Uses declarative routing with redirect logic based on `AuthBloc` and `ScheduleBloc` state |
| 197 | +- Routes defined with path and builder |
| 198 | +- NavigationService (`lib/core/services/navigation_service.dart`) provides programmatic navigation |
| 199 | + |
| 200 | +## Code Generation Files |
| 201 | + |
| 202 | +**Never manually edit** generated files (`.g.dart`, `.freezed.dart`, `di_setup.config.dart`). These are created by: |
| 203 | + |
| 204 | +- `build_runner` for JSON serialization, Freezed, Injectable, Drift |
| 205 | +- Regenerate after modifying annotated classes |
| 206 | + |
| 207 | +## Testing |
| 208 | + |
| 209 | +- Tests in `test/` mirror `lib/` structure |
| 210 | +- Use Mockito for mocking dependencies |
| 211 | +- Test helpers in `test/helpers/` |
| 212 | +- Run coverage: `flutter test --coverage` |
| 213 | + |
| 214 | +## Commit Message Convention |
| 215 | + |
| 216 | +Follow **Conventional Commits** (enforced by commitlint): |
| 217 | + |
| 218 | +- Format: `<type>(<optional scope>): <description>` |
| 219 | +- Types: `feat`, `fix`, `refactor`, `perf`, `style`, `test`, `docs`, `build`, `ops`, `chore` |
| 220 | +- Breaking changes: Add `!` before `:` (e.g., `feat(api)!: remove endpoint`) |
| 221 | +- See `docs/Git.md` for detailed examples |
| 222 | + |
| 223 | +## Key Project Files |
| 224 | + |
| 225 | +- `lib/main.dart`: App entry point, Firebase initialization, DI setup |
| 226 | +- `lib/presentation/app/screens/app.dart`: Root app widget |
| 227 | +- `lib/core/di/di_setup.dart`: DI configuration |
| 228 | +- `lib/core/database/database.dart`: Drift database setup |
| 229 | +- `docs/Architecture.md`: Comprehensive architecture documentation |
| 230 | +- `docs/Schedule-Timer-System.md`: Timer system details for ScheduleBloc |
| 231 | + |
| 232 | +## Common Patterns |
| 233 | + |
| 234 | +1. **Creating a new feature**: |
| 235 | + |
| 236 | + - Add entity in `domain/entities/` |
| 237 | + - Create repository interface in `domain/repositories/` |
| 238 | + - Add use cases in `domain/use-cases/` |
| 239 | + - Implement repository in `data/repositories/` |
| 240 | + - Add data sources (remote/local) in `data/data_sources/` |
| 241 | + - Build UI with BLoC in `presentation/<feature>/` |
| 242 | + - Annotate all with `@Injectable()` and run build_runner |
| 243 | + |
| 244 | +2. **Adding API endpoint**: |
| 245 | + |
| 246 | + - Create request/response models in `data/models/` with `@JsonSerializable()` |
| 247 | + - Add method to RemoteDataSource interface and implementation |
| 248 | + - Update repository to use new data source method |
| 249 | + - Run `dart run build_runner build` |
| 250 | + |
| 251 | +3. **Adding database table**: |
| 252 | + - Define table in `data/tables/` |
| 253 | + - Create DAO in `data/daos/` |
| 254 | + - Add to `@DriftDatabase` annotation in `database.dart` |
| 255 | + - Increment `schemaVersion` and add migration if needed |
| 256 | + - Run `dart run build_runner build` |
| 257 | + |
| 258 | +## Environment & Configuration |
| 259 | + |
| 260 | +- Environment variables in `lib/core/constants/environment_variable.dart` |
| 261 | +- Firebase configuration in `firebase_options.dart` |
| 262 | +- Analysis options in `analysis_options.yaml` |
| 263 | + |
| 264 | +## Widgetbook |
| 265 | + |
| 266 | +- Component showcase at `widgetbook/` (separate sub-project) |
| 267 | +- Hosted at: https://on-time-front-widgetbook.web.app/ |
| 268 | +- Use for UI component development and testing |
0 commit comments