16 KiB
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
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<Failure, T>for error handling - Implementation is provided by the data layer
abstract class WarehouseRepository {
Future<Either<Failure, List<WarehouseEntity>>> getWarehouses();
}
Use Cases
domain/usecases/get_warehouses_usecase.dart
- Single responsibility: fetch warehouses
- Encapsulates business logic
- Depends only on repository interface
class GetWarehousesUseCase {
final WarehouseRepository repository;
Future<Either<Failure, List<WarehouseEntity>>> 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)
class WarehouseModel extends WarehouseEntity {
factory WarehouseModel.fromJson(Map<String, dynamic> 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
ApiResponsewrapper - Throws typed exceptions (
ServerException,NetworkException)
class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource {
Future<List<WarehouseModel>> 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
class WarehouseRepositoryImpl implements WarehouseRepository {
@override
Future<Either<Failure, List<WarehouseEntity>>> 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 classwarehouses: List of warehousesselectedWarehouse: Currently selected warehouseisLoading: Loading indicatorerror: Error message
-
WarehouseNotifier: StateNotifier managing stateloadWarehouses(): Fetch warehouses from APIselectWarehouse(): Select a warehouserefresh(): Reload warehousesclearError(): Clear error state
class WarehouseState {
final List<WarehouseEntity> warehouses;
final WarehouseEntity? selectedWarehouse;
final bool isLoading;
final String? error;
}
class WarehouseNotifier extends StateNotifier<WarehouseState> {
Future<void> 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
curl -X GET https://api.example.com/warehouses \
-H "Authorization: Bearer {access_token}"
Response Format
{
"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:
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):
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../features/warehouse/warehouse_exports.dart';
// Core providers (if not already set up)
final secureStorageProvider = Provider<SecureStorage>((ref) {
return SecureStorage();
});
final apiClientProvider = Provider<ApiClient>((ref) {
final secureStorage = ref.watch(secureStorageProvider);
return ApiClient(secureStorage);
});
// Warehouse data layer providers
final warehouseRemoteDataSourceProvider = Provider<WarehouseRemoteDataSource>((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<WarehouseNotifier, WarehouseState>((ref) {
final getWarehousesUseCase = ref.watch(getWarehousesUseCaseProvider);
return WarehouseNotifier(getWarehousesUseCase);
});
3. Update WarehouseSelectionPage
Replace the TODO comments in warehouse_selection_page.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:
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
// From login page after successful authentication
context.go('/warehouses');
// Or using Navigator
Navigator.of(context).pushNamed('/warehouses');
Usage Examples
Loading Warehouses
// In a widget
ElevatedButton(
onPressed: () {
ref.read(warehouseProvider.notifier).loadWarehouses();
},
child: const Text('Load Warehouses'),
)
Watching State
// 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
// Select warehouse and navigate
void onWarehouseTap(WarehouseEntity warehouse) {
ref.read(warehouseProvider.notifier).selectWarehouse(warehouse);
context.push('/operations', extra: warehouse);
}
Pull to Refresh
RefreshIndicator(
onRefresh: () => ref.read(warehouseProvider.notifier).refresh(),
child: ListView(...),
)
Accessing Selected Warehouse
// 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:
// In use case or repository
Future<Either<Failure, List<WarehouseEntity>>> 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 errorsNetworkFailure: Connection issues, timeoutsCacheFailure: Local storage errors (if implemented)
Testing
Unit Tests
Test Use Case:
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:
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:
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
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
- Always use Either for error handling - Don't throw exceptions across layers
- Keep domain layer pure - No Flutter/external dependencies
- Use value objects - Entities should be immutable
- Single responsibility - Each class has one reason to change
- Dependency inversion - Depend on abstractions, not concretions
- 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:
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
- Flutter Riverpod Documentation
- Dartz Package for Functional Programming
- Material Design 3
Last Updated: 2025-10-27 Version: 1.0.0 Author: Claude Code