# Warehouse Feature Complete implementation of the warehouse feature following **Clean Architecture** principles. ## Architecture Overview This feature follows a three-layer clean architecture pattern: ``` Presentation Layer (UI) ↓ (uses) Domain Layer (Business Logic) ↓ (uses) Data Layer (API & Data Sources) ``` ### Key Principles - **Separation of Concerns**: Each layer has a single responsibility - **Dependency Inversion**: Outer layers depend on inner layers, not vice versa - **Testability**: Each layer can be tested independently - **Maintainability**: Changes in one layer don't affect others ## Project Structure ``` lib/features/warehouse/ ├── data/ │ ├── datasources/ │ │ └── warehouse_remote_datasource.dart # API calls using ApiClient │ ├── models/ │ │ └── warehouse_model.dart # Data transfer objects with JSON serialization │ └── repositories/ │ └── warehouse_repository_impl.dart # Repository implementation ├── domain/ │ ├── entities/ │ │ └── warehouse_entity.dart # Pure business models │ ├── repositories/ │ │ └── warehouse_repository.dart # Repository interface/contract │ └── usecases/ │ └── get_warehouses_usecase.dart # Business logic use cases ├── presentation/ │ ├── pages/ │ │ └── warehouse_selection_page.dart # Main warehouse selection screen │ ├── providers/ │ │ └── warehouse_provider.dart # Riverpod state management │ └── widgets/ │ └── warehouse_card.dart # Reusable warehouse card widget ├── warehouse_exports.dart # Barrel file for clean imports ├── warehouse_provider_setup_example.dart # Provider setup guide └── README.md # This file ``` ## Layer Details ### 1. Domain Layer (Core Business Logic) The innermost layer that contains business entities, repository interfaces, and use cases. **No dependencies on external frameworks or packages** (except dartz for Either). #### Entities `domain/entities/warehouse_entity.dart` - Pure Dart class representing a warehouse - No JSON serialization logic - Contains business rules and validations - Extends Equatable for value comparison ```dart class WarehouseEntity extends Equatable { final int id; final String name; final String code; final String? description; final bool isNGWareHouse; final int totalCount; bool get hasItems => totalCount > 0; bool get isNGType => isNGWareHouse; } ``` #### Repository Interface `domain/repositories/warehouse_repository.dart` - Abstract interface defining data operations - Returns `Either` for error handling - Implementation is provided by the data layer ```dart abstract class WarehouseRepository { Future>> getWarehouses(); } ``` #### Use Cases `domain/usecases/get_warehouses_usecase.dart` - Single responsibility: fetch warehouses - Encapsulates business logic - Depends only on repository interface ```dart class GetWarehousesUseCase { final WarehouseRepository repository; Future>> call() async { return await repository.getWarehouses(); } } ``` ### 2. Data Layer (External Data Management) Handles all data operations including API calls, JSON serialization, and error handling. #### Models `data/models/warehouse_model.dart` - Extends domain entity - Adds JSON serialization (`fromJson`, `toJson`) - Maps API response format to domain entities - Matches API field naming (PascalCase) ```dart class WarehouseModel extends WarehouseEntity { factory WarehouseModel.fromJson(Map json) { return WarehouseModel( id: json['Id'] ?? 0, name: json['Name'] ?? '', code: json['Code'] ?? '', description: json['Description'], isNGWareHouse: json['IsNGWareHouse'] ?? false, totalCount: json['TotalCount'] ?? 0, ); } } ``` #### Data Sources `data/datasources/warehouse_remote_datasource.dart` - Interface + implementation pattern - Makes API calls using `ApiClient` - Parses `ApiResponse` wrapper - Throws typed exceptions (`ServerException`, `NetworkException`) ```dart class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource { Future> getWarehouses() async { final response = await apiClient.get('/warehouses'); final apiResponse = ApiResponse.fromJson( response.data, (json) => (json as List).map((e) => WarehouseModel.fromJson(e)).toList(), ); if (apiResponse.isSuccess && apiResponse.value != null) { return apiResponse.value!; } else { throw ServerException(apiResponse.errors.first); } } } ``` #### Repository Implementation `data/repositories/warehouse_repository_impl.dart` - Implements domain repository interface - Coordinates data sources - Converts exceptions to failures - Maps models to entities ```dart class WarehouseRepositoryImpl implements WarehouseRepository { @override Future>> getWarehouses() async { try { final warehouses = await remoteDataSource.getWarehouses(); final entities = warehouses.map((model) => model.toEntity()).toList(); return Right(entities); } on ServerException catch (e) { return Left(ServerFailure(e.message)); } on NetworkException catch (e) { return Left(NetworkFailure(e.message)); } } } ``` ### 3. Presentation Layer (UI & State Management) Handles UI rendering, user interactions, and state management using Riverpod. #### State Management `presentation/providers/warehouse_provider.dart` - `WarehouseState`: Immutable state class - `warehouses`: List of warehouses - `selectedWarehouse`: Currently selected warehouse - `isLoading`: Loading indicator - `error`: Error message - `WarehouseNotifier`: StateNotifier managing state - `loadWarehouses()`: Fetch warehouses from API - `selectWarehouse()`: Select a warehouse - `refresh()`: Reload warehouses - `clearError()`: Clear error state ```dart class WarehouseState { final List warehouses; final WarehouseEntity? selectedWarehouse; final bool isLoading; final String? error; } class WarehouseNotifier extends StateNotifier { Future loadWarehouses() async { state = state.setLoading(); final result = await getWarehousesUseCase(); result.fold( (failure) => state = state.setError(failure.message), (warehouses) => state = state.setSuccess(warehouses), ); } } ``` #### Pages `presentation/pages/warehouse_selection_page.dart` - ConsumerStatefulWidget using Riverpod - Loads warehouses on initialization - Displays different UI states: - Loading: CircularProgressIndicator - Error: Error message with retry button - Empty: No warehouses message - Success: List of warehouse cards - Pull-to-refresh support - Navigation to operations page on selection #### Widgets `presentation/widgets/warehouse_card.dart` - Reusable warehouse card component - Displays: - Warehouse name (title) - Code (with QR icon) - Total items count (with inventory icon) - Description (if available) - NG warehouse badge (if applicable) - Material Design 3 styling - Tap to select functionality ## API Integration ### Endpoint ``` GET /warehouses ``` ### Request ```bash curl -X GET https://api.example.com/warehouses \ -H "Authorization: Bearer {access_token}" ``` ### Response Format ```json { "Value": [ { "Id": 1, "Name": "Kho nguyên vật liệu", "Code": "001", "Description": "Kho chứa nguyên vật liệu", "IsNGWareHouse": false, "TotalCount": 8 }, { "Id": 2, "Name": "Kho bán thành phẩm công đoạn", "Code": "002", "Description": null, "IsNGWareHouse": false, "TotalCount": 12 } ], "IsSuccess": true, "IsFailure": false, "Errors": [], "ErrorCodes": [] } ``` ## Setup & Integration ### 1. Install Dependencies Ensure your `pubspec.yaml` includes: ```yaml dependencies: flutter_riverpod: ^2.4.9 dio: ^5.3.2 dartz: ^0.10.1 equatable: ^2.0.5 flutter_secure_storage: ^9.0.0 ``` ### 2. Set Up Providers Create or update your provider configuration file (e.g., `lib/core/di/providers.dart`): ```dart import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../features/warehouse/warehouse_exports.dart'; // Core providers (if not already set up) final secureStorageProvider = Provider((ref) { return SecureStorage(); }); final apiClientProvider = Provider((ref) { final secureStorage = ref.watch(secureStorageProvider); return ApiClient(secureStorage); }); // Warehouse data layer providers final warehouseRemoteDataSourceProvider = Provider((ref) { final apiClient = ref.watch(apiClientProvider); return WarehouseRemoteDataSourceImpl(apiClient); }); final warehouseRepositoryProvider = Provider((ref) { final remoteDataSource = ref.watch(warehouseRemoteDataSourceProvider); return WarehouseRepositoryImpl(remoteDataSource); }); // Warehouse domain layer providers final getWarehousesUseCaseProvider = Provider((ref) { final repository = ref.watch(warehouseRepositoryProvider); return GetWarehousesUseCase(repository); }); // Warehouse presentation layer providers final warehouseProvider = StateNotifierProvider((ref) { final getWarehousesUseCase = ref.watch(getWarehousesUseCaseProvider); return WarehouseNotifier(getWarehousesUseCase); }); ``` ### 3. Update WarehouseSelectionPage Replace the TODO comments in `warehouse_selection_page.dart`: ```dart @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(warehouseProvider.notifier).loadWarehouses(); }); } @override Widget build(BuildContext context) { final state = ref.watch(warehouseProvider); // Rest of the implementation... } ``` ### 4. Add to Router Using go_router: ```dart GoRoute( path: '/warehouses', name: 'warehouses', builder: (context, state) => const WarehouseSelectionPage(), ), GoRoute( path: '/operations', name: 'operations', builder: (context, state) { final warehouse = state.extra as WarehouseEntity; return OperationSelectionPage(warehouse: warehouse); }, ), ``` ### 5. Navigate to Warehouse Page ```dart // From login page after successful authentication context.go('/warehouses'); // Or using Navigator Navigator.of(context).pushNamed('/warehouses'); ``` ## Usage Examples ### Loading Warehouses ```dart // In a widget ElevatedButton( onPressed: () { ref.read(warehouseProvider.notifier).loadWarehouses(); }, child: const Text('Load Warehouses'), ) ``` ### Watching State ```dart // In a ConsumerWidget @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(warehouseProvider); if (state.isLoading) { return const CircularProgressIndicator(); } if (state.error != null) { return Text('Error: ${state.error}'); } return ListView.builder( itemCount: state.warehouses.length, itemBuilder: (context, index) { final warehouse = state.warehouses[index]; return ListTile(title: Text(warehouse.name)); }, ); } ``` ### Selecting a Warehouse ```dart // Select warehouse and navigate void onWarehouseTap(WarehouseEntity warehouse) { ref.read(warehouseProvider.notifier).selectWarehouse(warehouse); context.push('/operations', extra: warehouse); } ``` ### Pull to Refresh ```dart RefreshIndicator( onRefresh: () => ref.read(warehouseProvider.notifier).refresh(), child: ListView(...), ) ``` ### Accessing Selected Warehouse ```dart // In another page final state = ref.watch(warehouseProvider); final selectedWarehouse = state.selectedWarehouse; if (selectedWarehouse != null) { Text('Current: ${selectedWarehouse.name}'); } ``` ## Error Handling The feature uses dartz's `Either` type for functional error handling: ```dart // In use case or repository Future>> getWarehouses() async { try { final warehouses = await remoteDataSource.getWarehouses(); return Right(warehouses); // Success } on ServerException catch (e) { return Left(ServerFailure(e.message)); // Failure } } // In presentation layer result.fold( (failure) => print('Error: ${failure.message}'), (warehouses) => print('Success: ${warehouses.length} items'), ); ``` ### Failure Types - `ServerFailure`: API errors, HTTP errors - `NetworkFailure`: Connection issues, timeouts - `CacheFailure`: Local storage errors (if implemented) ## Testing ### Unit Tests **Test Use Case:** ```dart test('should get warehouses from repository', () async { // Arrange when(mockRepository.getWarehouses()) .thenAnswer((_) async => Right(mockWarehouses)); // Act final result = await useCase(); // Assert expect(result, Right(mockWarehouses)); verify(mockRepository.getWarehouses()); }); ``` **Test Repository:** ```dart test('should return warehouses when remote call is successful', () async { // Arrange when(mockRemoteDataSource.getWarehouses()) .thenAnswer((_) async => mockWarehouseModels); // Act final result = await repository.getWarehouses(); // Assert expect(result.isRight(), true); }); ``` **Test Notifier:** ```dart test('should emit loading then success when warehouses are loaded', () async { // Arrange when(mockUseCase()).thenAnswer((_) async => Right(mockWarehouses)); // Act await notifier.loadWarehouses(); // Assert expect(notifier.state.isLoading, false); expect(notifier.state.warehouses, mockWarehouses); }); ``` ### Widget Tests ```dart testWidgets('should display warehouse list when loaded', (tester) async { // Arrange final container = ProviderContainer( overrides: [ warehouseProvider.overrideWith((ref) => MockWarehouseNotifier()), ], ); // Act await tester.pumpWidget( UncontrolledProviderScope( container: container, child: const WarehouseSelectionPage(), ), ); // Assert expect(find.byType(WarehouseCard), findsWidgets); }); ``` ## Best Practices 1. **Always use Either for error handling** - Don't throw exceptions across layers 2. **Keep domain layer pure** - No Flutter/external dependencies 3. **Use value objects** - Entities should be immutable 4. **Single responsibility** - Each class has one reason to change 5. **Dependency inversion** - Depend on abstractions, not concretions 6. **Test each layer independently** - Use mocks and test doubles ## Common Issues ### Provider Not Found **Error:** `ProviderNotFoundException` **Solution:** Make sure you've set up all providers in your provider configuration file and wrapped your app with `ProviderScope`. ### Null Safety Issues **Error:** `Null check operator used on a null value` **Solution:** Always check for null before accessing optional fields: ```dart if (warehouse.description != null) { Text(warehouse.description!); } ``` ### API Response Format Mismatch **Error:** `ServerException: Invalid response format` **Solution:** Verify that the API response matches the expected format in `ApiResponse.fromJson` and `WarehouseModel.fromJson`. ## Future Enhancements - [ ] Add caching with Hive for offline support - [ ] Implement warehouse search functionality - [ ] Add warehouse filtering (by type, name, etc.) - [ ] Add pagination for large warehouse lists - [ ] Implement warehouse CRUD operations - [ ] Add warehouse analytics and statistics ## Related Features - **Authentication**: `/lib/features/auth/` - User login and token management - **Operations**: `/lib/features/operation/` - Import/Export selection - **Products**: `/lib/features/products/` - Product listing per warehouse ## References - [Clean Architecture by Uncle Bob](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) - [Flutter Riverpod Documentation](https://riverpod.dev/) - [Dartz Package for Functional Programming](https://pub.dev/packages/dartz) - [Material Design 3](https://m3.material.io/) --- **Last Updated:** 2025-10-27 **Version:** 1.0.0 **Author:** Claude Code