# Architecture Guide This document defines the Clean Architecture patterns and conventions used in Tatlock UI. ## Overview Tatlock UI follows **Clean Architecture** with feature-based organization. This separation ensures testability, maintainability, and clear boundaries between concerns. ## Project Structure The directory structure mirrors the "Rooms of the Estate" UI navigation (see [UI_LAYOUT.md](./UI_LAYOUT.md)). ``` lib/ ├── main.dart # Entry point ├── version.g.dart # Generated version info ├── app.dart # MaterialApp.router setup ├── core/ # Shared infrastructure │ ├── api/ # HTTP client, interceptors, SSE │ ├── auth/ # Authentik OIDC integration │ ├── config/ # Environment configuration │ ├── error/ # Error types and handling │ └── theme/ # Material 3 theming ├── routing/ # go_router configuration ├── shared/ # Reusable components │ ├── components/ # DataGrid, common widgets │ └── layouts/ # App scaffold, navigation ├── features/ # Feature modules (rooms) │ ├── front_hall/ # Dashboard - estate overview │ ├── control_room/ # Infrastructure │ │ ├── containers/ # Container management │ │ ├── stacks/ # Stack management │ │ ├── networks/ # Network management │ │ └── volumes/ # Volume management │ ├── parlor/ # Housekeeping - home automation │ ├── library/ # Knowledge management (future) │ └── study/ # Secretarial tasks (future) └── chat/ # Tatlock chat - omnipresent, NOT a room ``` ### Room-to-Directory Mapping | UI Room | Directory | Purpose | |---------|-----------|---------| | Front Hall | `features/front_hall/` | Dashboard, overview, quick access | | Control Room | `features/control_room/` | Infrastructure management | | Parlor | `features/parlor/` | Home automation | | Library | `features/library/` | Knowledge, docs, bookmarks | | Study | `features/study/` | Email, calendar (hidden for now) | | *(omnipresent)* | `chat/` | Tatlock assistant dock | Note: `chat/` lives at the top level of `lib/` (not under `features/`) because it's not a navigable room - it's an omnipresent dock injected at the layout level. ## Feature Structure Each feature follows a three-layer architecture: ``` features/{feature}/ ├── presentation/ # UI Layer │ ├── pages/ # Full-screen route widgets │ ├── widgets/ # Feature-specific widgets │ └── providers/ # Riverpod providers/notifiers ├── domain/ # Business Logic Layer │ ├── entities/ # Immutable business objects │ ├── repositories/ # Abstract repository interfaces │ └── usecases/ # Single-purpose business operations └── data/ # Data Layer ├── models/ # JSON-serializable DTOs ├── datasources/ # API clients, local storage └── repositories/ # Repository implementations ``` ## Layer Rules ### Domain Layer (Pure Dart) The domain layer is the core of each feature. It has **no dependencies** on Flutter, external packages, or other layers. **Entities** - Immutable business objects: ```dart // domain/entities/container.dart class Container { const Container({ required this.id, required this.name, required this.status, required this.image, }); final String id; final String name; final ContainerStatus status; final String image; } enum ContainerStatus { running, stopped, paused, restarting } ``` **Repository Interfaces** - Abstract contracts: ```dart // domain/repositories/container_repository.dart abstract class ContainerRepository { Future> getContainers(); Future getContainer(String id); Future startContainer(String id); Future stopContainer(String id); } ``` **Use Cases** - Single-purpose operations (optional, for complex logic): ```dart // domain/usecases/restart_unhealthy_containers.dart class RestartUnhealthyContainers { RestartUnhealthyContainers(this._repository); final ContainerRepository _repository; Future call() async { final containers = await _repository.getContainers(); final unhealthy = containers.where((c) => c.status == ContainerStatus.stopped); for (final container in unhealthy) { await _repository.startContainer(container.id); } return unhealthy.length; } } ``` ### Data Layer (External Dependencies) The data layer implements domain interfaces and handles external communication. **Models** - JSON-serializable DTOs: ```dart // data/models/container_model.dart import 'package:freezed_annotation/freezed_annotation.dart'; part 'container_model.freezed.dart'; part 'container_model.g.dart'; @freezed class ContainerModel with _$ContainerModel { const factory ContainerModel({ required String id, required String name, required String status, required String image, }) = _ContainerModel; factory ContainerModel.fromJson(Map json) => _$ContainerModelFromJson(json); } extension ContainerModelX on ContainerModel { Container toEntity() => Container( id: id, name: name, status: ContainerStatus.values.byName(status), image: image, ); } ``` **Data Sources** - API clients: ```dart // data/datasources/containers_datasource.dart class ContainersDatasource { ContainersDatasource(this._client); final ApiClient _client; Future> getContainers() async { final response = await _client.get('/infrastructure/containers'); return (response.data as List) .map((json) => ContainerModel.fromJson(json)) .toList(); } } ``` **Repository Implementations**: ```dart // data/repositories/container_repository_impl.dart class ContainerRepositoryImpl implements ContainerRepository { ContainerRepositoryImpl(this._datasource); final ContainersDatasource _datasource; @override Future> getContainers() async { final models = await _datasource.getContainers(); return models.map((m) => m.toEntity()).toList(); } } ``` ### Presentation Layer (Flutter + Riverpod) The presentation layer contains UI code and state management. **Providers** - Riverpod state management: ```dart // presentation/providers/containers_provider.dart import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'containers_provider.g.dart'; @riverpod class ContainersNotifier extends _$ContainersNotifier { @override Future> build() async { final repository = ref.watch(containerRepositoryProvider); return repository.getContainers(); } Future refresh() async { state = const AsyncLoading(); state = await AsyncValue.guard(() => ref.read(containerRepositoryProvider).getContainers()); } } ``` **Pages** - Full-screen routes: ```dart // presentation/pages/containers_page.dart class ContainersPage extends ConsumerWidget { const ContainersPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final containersAsync = ref.watch(containersNotifierProvider); return containersAsync.when( data: (containers) => ContainersList(containers: containers), loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => ErrorDisplay(error: error), ); } } ``` **Widgets** - Feature-specific UI components: ```dart // presentation/widgets/container_card.dart class ContainerCard extends StatelessWidget { const ContainerCard({super.key, required this.container}); final Container container; @override Widget build(BuildContext context) { // Widget implementation } } ``` ## Dependency Flow ``` Presentation → Domain ← Data ↓ ↑ ↓ Riverpod Interfaces Dio/API ``` - Presentation depends on Domain (uses entities, calls repository interfaces) - Data depends on Domain (implements repository interfaces) - Domain depends on nothing (pure Dart) ## Dependency Injection Use Riverpod for all dependency injection: ```dart // Datasource provider @riverpod ContainersDatasource containersDatasource(Ref ref) { return ContainersDatasource(ref.watch(apiClientProvider)); } // Repository provider (returns interface type) @riverpod ContainerRepository containerRepository(Ref ref) { return ContainerRepositoryImpl(ref.watch(containersDatasourceProvider)); } ``` ## File Naming Conventions | Type | Convention | Example | |------|------------|---------| | Pages | `{name}_page.dart` | `containers_page.dart` | | Widgets | `{name}_{type}.dart` | `container_card.dart` | | Providers | `{name}_provider.dart` | `containers_provider.dart` | | Entities | `{name}.dart` | `container.dart` | | Models | `{name}_model.dart` | `container_model.dart` | | Repositories | `{name}_repository.dart` | `container_repository.dart` | | Datasources | `{name}_datasource.dart` | `containers_datasource.dart` | | Use Cases | `{action}_{noun}.dart` | `restart_container.dart` | ## Testing Requirements Each layer has specific testing requirements: ### Domain Layer - **Unit tests** for all entities and use cases - No mocking needed (pure Dart) - 100% coverage expected ### Data Layer - **Unit tests** for model serialization - **Integration tests** for datasources (mock HTTP) - Test entity conversions ### Presentation Layer - **Widget tests** for all pages and complex widgets - **Provider tests** for state management logic - Mock repositories using `mocktail` ## Code Generation Run after modifying annotated code: ```bash dart run build_runner build --delete-conflicting-outputs ``` Generated files: - `*.freezed.dart` - Immutable classes - `*.g.dart` - JSON serialization, Riverpod providers ## Import Rules 1. Never import from `data/` in `domain/` 2. Never import from `presentation/` in `domain/` or `data/` 3. Import entities from `domain/`, not models from `data/` 4. Use relative imports within a feature, package imports across features ```dart // Good - within feature import '../domain/entities/container.dart'; // Good - across features import 'package:tatlock_ui/features/containers/domain/entities/container.dart'; // Bad - importing model in presentation import '../data/models/container_model.dart'; // Don't do this ```